chore: update analytics sidebar and header content, fix: trash box positioning (#1065)

* fix: labels dropdown on issue details page theming

* style: trash box styling and positioning

* chore: empty state for scope and demand analytics, show assignee name in scope graph tooltip

* chore: empty state for analytics

* chore: modify analytics sidebar and header
This commit is contained in:
Aaryan Khandelwal 2023-05-17 13:25:58 +05:30 committed by GitHub
parent 559b0cc9c8
commit 3427652c22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 232 additions and 152 deletions

View File

@ -13,7 +13,12 @@ import useToast from "hooks/use-toast";
// ui // ui
import { PrimaryButton, SecondaryButton } from "components/ui"; import { PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { ArrowDownTrayIcon, ArrowPathIcon, UserGroupIcon } from "@heroicons/react/24/outline"; import {
ArrowDownTrayIcon,
ArrowPathIcon,
CalendarDaysIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { ContrastIcon, LayerDiagonalIcon } from "components/icons"; import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
// helpers // helpers
import { renderShortDate } from "helpers/date-time.helper"; import { renderShortDate } from "helpers/date-time.helper";
@ -106,11 +111,14 @@ export const AnalyticsSidebar: React.FC<Props> = ({
); );
}; };
const selectedProjects =
params.project && params.project.length > 0 ? params.project : projects.map((p) => p.id);
return ( return (
<div <div
className={`p-5 pb-0 flex flex-col space-y-2 ${ className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
fullScreen fullScreen
? "pb-5 border-l border-brand-base md:h-full md:pb-5 md:border-l md:border-brand-base md:space-y-4 overflow-hidden" ? "border-l border-brand-base md:h-full md:border-l md:border-brand-base md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
: "" : ""
}`} }`}
> >
@ -119,15 +127,27 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<LayerDiagonalIcon height={14} width={14} /> <LayerDiagonalIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues {analytics ? analytics.total : "..."} Issues
</div> </div>
{isProjectLevel && (
<div className="flex items-center gap-1 bg-brand-surface-2 rounded-md px-3 py-1 text-brand-secondary text-xs">
<CalendarDaysIcon className="h-3.5 w-3.5" />
{renderShortDate(
(cycleId
? cycleDetails?.created_at
: moduleId
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
)}
</div>
)}
</div> </div>
<div className="h-full overflow-hidden"> <div className="h-full overflow-hidden">
{fullScreen ? ( {fullScreen ? (
<> <>
{!isProjectLevel && params.project && params.project.length > 0 && ( {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<div className="hidden h-full overflow-hidden md:flex md:flex-col"> <div className="hidden h-full overflow-hidden md:flex md:flex-col">
<h4 className="font-medium">Selected Projects</h4> <h4 className="font-medium">Selected Projects</h4>
<div className="space-y-6 mt-4 h-full overflow-y-auto"> <div className="space-y-6 mt-4 h-full overflow-y-auto">
{params.project.map((projectId) => { {selectedProjects.map((projectId) => {
const project: IProject = projects.find((p) => p.id === projectId); const project: IProject = projects.find((p) => p.id === projectId);
return ( return (

View File

@ -10,6 +10,9 @@ import { useForm } from "react-hook-form";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// services // services
import analyticsService from "services/analytics.service"; import analyticsService from "services/analytics.service";
import projectService from "services/project.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
// components // components
import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
// icons // icons
@ -21,7 +24,7 @@ import {
// types // types
import { IAnalyticsParams } from "types"; import { IAnalyticsParams } from "types";
// fetch-keys // fetch-keys
import { ANALYTICS } from "constants/fetch-keys"; import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -59,6 +62,39 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
); );
const { data: projectDetails } = useSWR(
workspaceSlug && projectId && !(cycleId || moduleId)
? PROJECT_DETAILS(projectId.toString())
: null,
workspaceSlug && projectId && !(cycleId || moduleId)
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: cycleDetails } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () =>
cyclesService.getCycleDetails(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
: null
);
const { data: moduleDetails } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
workspaceSlug && projectId && moduleId
? () =>
modulesService.getModuleDetails(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString()
)
: null
);
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
}; };
@ -74,12 +110,11 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
fullScreen ? "rounded-lg border" : "border-l" fullScreen ? "rounded-lg border" : "border-l"
}`} }`}
> >
<div <div className="flex items-center justify-between gap-4 bg-brand-base px-5 py-4 text-sm">
className={`flex items-center justify-between gap-2 border-b border-b-brand-base bg-brand-base p-3 text-sm ${ <h3 className="break-all">
fullScreen ? "" : "py-[1.275rem]" Analytics for{" "}
}`} {cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
> </h3>
<h3>Project Analytics</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
@ -102,7 +137,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
</div> </div>
</div> </div>
<Tab.Group as={Fragment}> <Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-brand-base px-5 py-3"> <Tab.List as="div" className="space-x-2 border-b border-brand-base p-5 pt-0">
{tabsList.map((tab) => ( {tabsList.map((tab) => (
<Tab <Tab
key={tab} key={tab}
@ -116,6 +151,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
</Tab> </Tab>
))} ))}
</Tab.List> </Tab.List>
{/* <h4 className="p-5 pb-0">Analytics for</h4> */}
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel as={Fragment}> <Tab.Panel as={Fragment}>
<ScopeAndDemand fullScreen={fullScreen} /> <ScopeAndDemand fullScreen={fullScreen} />

View File

@ -14,32 +14,39 @@ type Props = {
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => ( export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
<div className="p-3 border border-brand-base rounded-[10px]"> <div className="p-3 border border-brand-base rounded-[10px]">
<h6 className="text-base font-medium">{title}</h6> <h6 className="text-base font-medium">{title}</h6>
<div className="mt-3 space-y-3"> {users.length > 0 ? (
{users.map((user) => ( <div className="mt-3 space-y-3">
<div key={user.email ?? "None"} className="flex items-start justify-between gap-4 text-xs"> {users.map((user) => (
<div className="flex items-center gap-2"> <div
{user && user.avatar && user.avatar !== "" ? ( key={user.email ?? "None"}
<div className="rounded-full h-4 w-4 flex-shrink-0"> className="flex items-start justify-between gap-4 text-xs"
<Image >
src={user.avatar} <div className="flex items-center gap-2">
height="100%" {user && user.avatar && user.avatar !== "" ? (
width="100%" <div className="rounded-full h-4 w-4 flex-shrink-0">
className="rounded-full" <Image
alt={user.email ?? "None"} src={user.avatar}
/> height="100%"
</div> width="100%"
) : ( className="rounded-full"
<div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4"> alt={user.email ?? "None"}
{user.firstName !== "" ? user.firstName[0] : "?"} />
</div> </div>
)} ) : (
<span className="break-all text-brand-secondary"> <div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4">
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"} {user.firstName !== "" ? user.firstName[0] : "?"}
</span> </div>
)}
<span className="break-all text-brand-secondary">
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
</span>
</div>
<span className="flex-shrink-0">{user.count}</span>
</div> </div>
<span className="flex-shrink-0">{user.count}</span> ))}
</div> </div>
))} ) : (
</div> <div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div>
)}
</div> </div>
); );

View File

@ -13,56 +13,72 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="divide-y divide-brand-base"> <div className="divide-y divide-brand-base">
<div> <div>
<h6 className="px-3 text-base font-medium">Pending issues</h6> <h6 className="px-3 text-base font-medium">Pending issues</h6>
<BarGraph {defaultAnalytics.pending_issue_user.length > 0 ? (
data={defaultAnalytics.pending_issue_user} <BarGraph
indexBy="assignees__email" data={defaultAnalytics.pending_issue_user}
keys={["count"]} indexBy="assignees__email"
height="250px" keys={["count"]}
colors={() => `#f97316`} height="250px"
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)} colors={() => `#f97316`}
tooltip={(datum) => ( customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs"> tooltip={(datum) => {
<span className="font-medium text-brand-secondary"> const assignee = defaultAnalytics.pending_issue_user.find(
Issue count- {datum.indexValue ?? "No assignee"}:{" "} (a) => a.assignees__email === `${datum.indexValue}`
</span> );
{datum.value}
</div>
)}
axisBottom={{
renderTick: (datum) => {
const avatar =
defaultAnalytics.pending_issue_user[datum.tickIndex].assignees__avatar ?? "";
if (avatar && avatar !== "") return (
return ( <div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
<g transform={`translate(${datum.x},${datum.y})`}> <span className="font-medium text-brand-secondary">
<image Issue count-{" "}
x={-8} {assignee
y={10} ? assignee.assignees__first_name + " " + assignee.assignees__last_name
width={16} : "No assignee"}
height={16} :{" "}
xlinkHref={avatar} </span>
style={{ clipPath: "circle(50%)" }} {datum.value}
/> </div>
</g> );
); }}
else axisBottom={{
return ( renderTick: (datum) => {
<g transform={`translate(${datum.x},${datum.y})`}> const avatar =
<circle cy={18} r={8} fill="#374151" /> defaultAnalytics.pending_issue_user[datum.tickIndex].assignees__avatar ?? "";
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"} if (avatar && avatar !== "")
</text> return (
</g> <g transform={`translate(${datum.x},${datum.y})`}>
); <image
}, x={-8}
}} y={10}
margin={{ top: 20 }} width={16}
theme={{ height={16}
background: "rgb(var(--color-bg-base))", xlinkHref={avatar}
axis: {}, style={{ clipPath: "circle(50%)" }}
}} />
/> </g>
);
else
return (
<g transform={`translate(${datum.x},${datum.y})`}>
<circle cy={18} r={8} fill="#374151" />
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"}
</text>
</g>
);
},
}}
margin={{ top: 20 }}
theme={{
background: "rgb(var(--color-bg-base))",
axis: {},
}}
/>
) : (
<div className="text-brand-secondary text-center text-sm py-8">
No matching data found.
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -17,34 +17,38 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
return ( return (
<div className="py-3 border border-brand-base rounded-[10px]"> <div className="py-3 border border-brand-base rounded-[10px]">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1> <h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
<LineGraph {defaultAnalytics.issue_completed_month_wise.length > 0 ? (
data={[ <LineGraph
{ data={[
id: "issues_closed", {
color: "rgb(var(--color-accent))", id: "issues_closed",
data: MONTHS_LIST.map((month) => ({ color: "rgb(var(--color-accent))",
x: month.label.substring(0, 3), data: MONTHS_LIST.map((month) => ({
y: x: month.label.substring(0, 3),
defaultAnalytics.issue_completed_month_wise.find( y:
(data) => data.month === month.value defaultAnalytics.issue_completed_month_wise.find(
)?.count || 0, (data) => data.month === month.value
})), )?.count || 0,
}, })),
]} },
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => { ]}
if (quarterMonthsList.includes(data.month)) return data.count; customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => {
if (quarterMonthsList.includes(data.month)) return data.count;
return 0; return 0;
})} })}
height="300px" height="300px"
colors={(datum) => datum.color} colors={(datum) => datum.color}
curve="monotoneX" curve="monotoneX"
margin={{ top: 20 }} margin={{ top: 20 }}
theme={{ theme={{
background: "rgb(var(--color-bg-base))", background: "rgb(var(--color-bg-base))",
}} }}
enableArea enableArea
/> />
) : (
<div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div>
)}
</div> </div>
); );
}; };

View File

@ -6,6 +6,7 @@ import useSWR, { mutate } from "swr";
// react-beautiful-dnd // react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd"; import { DragDropContext, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import stateService from "services/state.service"; import stateService from "services/state.service";
@ -19,7 +20,6 @@ import useIssuesView from "hooks/use-issues-view";
// components // components
import { AllLists, AllBoards, FilterList, CalendarView } from "components/core"; import { AllLists, AllBoards, FilterList, CalendarView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui // ui
@ -47,7 +47,6 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST, STATES_LIST,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// image
type Props = { type Props = {
type?: "issue" | "cycle" | "module"; type?: "issue" | "cycle" | "module";
@ -445,27 +444,25 @@ export const IssuesView: React.FC<Props> = ({
<> <>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0"> <div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
<FilterList filters={filters} setFilters={setFilters} /> <FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && ( <PrimaryButton
<PrimaryButton onClick={() => {
onClick={() => { if (viewId) {
if (viewId) { setFilters({}, true);
setFilters({}, true); setToastAlert({
setToastAlert({ title: "View updated",
title: "View updated", message: "Your view has been updated",
message: "Your view has been updated", type: "success",
type: "success", });
}); } else
} else setCreateViewModal({
setCreateViewModal({ query: filters,
query: filters, });
}); }}
}} className="flex items-center gap-2 text-sm"
className="flex items-center gap-2 text-sm" >
> {!viewId && <PlusIcon className="h-4 w-4" />}
{!viewId && <PlusIcon className="h-4 w-4" />} {viewId ? "Update" : "Save"} view
{viewId ? "Update" : "Save"} view </PrimaryButton>
</PrimaryButton>
)}
</div> </div>
{<div className="mt-3 border-t border-brand-base" />} {<div className="mt-3 border-t border-brand-base" />}
</> </>
@ -477,14 +474,14 @@ export const IssuesView: React.FC<Props> = ({
<div <div
className={`${ className={`${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0" trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-red-500/20 p-3 text-xs font-medium italic text-red-500 ${ } fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-brand-base px-3 py-5 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500/100 text-white" : "" snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
} duration-200`} } transition duration-300`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
> >
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
Drop issue here to delete Drop here to delete the issue.
</div> </div>
)} )}
</StrictModeDroppable> </StrictModeDroppable>

View File

@ -74,7 +74,7 @@ export const IssueAttachmentUpload = () => {
onDrop, onDrop,
maxSize: maxFileSize, maxSize: maxFileSize,
multiple: false, multiple: false,
disabled: isLoading disabled: isLoading,
}); });
const fileError = const fileError =
@ -85,8 +85,8 @@ export const IssueAttachmentUpload = () => {
return ( return (
<div <div
{...getRootProps()} {...getRootProps()}
className={`flex items-center justify-center h-[60px] cursor-pointer border-2 border-dashed border-theme text-blue-500 bg-blue-500/5 text-xs rounded-md px-4 ${ className={`flex items-center justify-center h-[60px] cursor-pointer border-2 border-dashed text-brand-accent bg-brand-accent/5 text-xs rounded-md px-4 ${
isDragActive ? "bg-theme/10" : "" isDragActive ? "bg-brand-accent/10 border-brand-accent" : "border-brand-base"
} ${isDragReject ? "bg-red-100" : ""}`} } ${isDragReject ? "bg-red-100" : ""}`}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />

View File

@ -450,7 +450,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-brand-surface-2 py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-brand-surface-2 py-1 text-xs shadow-lg border border-brand-base focus:outline-none">
<div className="py-1"> <div className="py-1">
{issueLabels ? ( {issueLabels ? (
issueLabels.length > 0 ? ( issueLabels.length > 0 ? (
@ -468,8 +468,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
`${ `${
active || selected ? "bg-brand-surface-1" : "" active || selected ? "bg-brand-surface-1" : ""
} ${ } ${
selected ? "font-medium" : "" selected ? "" : "text-brand-secondary"
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-brand-base` } flex cursor-pointer select-none items-center gap-2 truncate p-2`
} }
value={label.id} value={label.id}
> >
@ -489,7 +489,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
return ( return (
<div className="border-y border-brand-base bg-brand-surface-1"> <div className="border-y border-brand-base bg-brand-surface-1">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-brand-base"> <div className="flex select-none items-center gap-2 truncate p-2 font-medium text-brand-base">
<RectangleGroupIcon className="h-3 w-3" />{" "} <RectangleGroupIcon className="h-3 w-3" />
{label.name} {label.name}
</div> </div>
<div> <div>
@ -497,9 +497,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<Listbox.Option <Listbox.Option
key={child.id} key={child.id}
className={({ active, selected }) => className={({ active, selected }) =>
`${active || selected ? "bg-indigo-50" : ""} ${ `${active || selected ? "bg-brand-base" : ""} ${
selected ? "font-medium" : "" selected ? "" : "text-brand-secondary"
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-brand-base` } flex cursor-pointer select-none items-center gap-2 truncate p-2`
} }
value={child.id} value={child.id}
> >