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
import { PrimaryButton, SecondaryButton } from "components/ui";
// 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";
// helpers
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 (
<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
? "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} />
{analytics ? analytics.total : "..."} Issues
</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 className="h-full overflow-hidden">
{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">
<h4 className="font-medium">Selected Projects</h4>
<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);
return (

View File

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

View File

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

View File

@ -13,56 +13,72 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="divide-y divide-brand-base">
<div>
<h6 className="px-3 text-base font-medium">Pending issues</h6>
<BarGraph
data={defaultAnalytics.pending_issue_user}
indexBy="assignees__email"
keys={["count"]}
height="250px"
colors={() => `#f97316`}
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
tooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
<span className="font-medium text-brand-secondary">
Issue count- {datum.indexValue ?? "No assignee"}:{" "}
</span>
{datum.value}
</div>
)}
axisBottom={{
renderTick: (datum) => {
const avatar =
defaultAnalytics.pending_issue_user[datum.tickIndex].assignees__avatar ?? "";
{defaultAnalytics.pending_issue_user.length > 0 ? (
<BarGraph
data={defaultAnalytics.pending_issue_user}
indexBy="assignees__email"
keys={["count"]}
height="250px"
colors={() => `#f97316`}
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
tooltip={(datum) => {
const assignee = defaultAnalytics.pending_issue_user.find(
(a) => a.assignees__email === `${datum.indexValue}`
);
if (avatar && avatar !== "")
return (
<g transform={`translate(${datum.x},${datum.y})`}>
<image
x={-8}
y={10}
width={16}
height={16}
xlinkHref={avatar}
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: {},
}}
/>
return (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
<span className="font-medium text-brand-secondary">
Issue count-{" "}
{assignee
? assignee.assignees__first_name + " " + assignee.assignees__last_name
: "No assignee"}
:{" "}
</span>
{datum.value}
</div>
);
}}
axisBottom={{
renderTick: (datum) => {
const avatar =
defaultAnalytics.pending_issue_user[datum.tickIndex].assignees__avatar ?? "";
if (avatar && avatar !== "")
return (
<g transform={`translate(${datum.x},${datum.y})`}>
<image
x={-8}
y={10}
width={16}
height={16}
xlinkHref={avatar}
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>

View File

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

View File

@ -6,6 +6,7 @@ import useSWR, { mutate } from "swr";
// react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
@ -19,7 +20,6 @@ import useIssuesView from "hooks/use-issues-view";
// components
import { AllLists, AllBoards, FilterList, CalendarView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateViewModal } from "components/views";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui
@ -47,7 +47,6 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST,
} from "constants/fetch-keys";
// image
type Props = {
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">
<FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && (
<PrimaryButton
onClick={() => {
if (viewId) {
setFilters({}, true);
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!viewId && <PlusIcon className="h-4 w-4" />}
{viewId ? "Update" : "Save"} view
</PrimaryButton>
)}
<PrimaryButton
onClick={() => {
if (viewId) {
setFilters({}, true);
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!viewId && <PlusIcon className="h-4 w-4" />}
{viewId ? "Update" : "Save"} view
</PrimaryButton>
</div>
{<div className="mt-3 border-t border-brand-base" />}
</>
@ -477,14 +474,14 @@ export const IssuesView: React.FC<Props> = ({
<div
className={`${
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 ${
snapshot.isDraggingOver ? "bg-red-500/100 text-white" : ""
} duration-200`}
} 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 blur-2xl opacity-70" : ""
} transition duration-300`}
ref={provided.innerRef}
{...provided.droppableProps}
>
<TrashIcon className="h-4 w-4" />
Drop issue here to delete
Drop here to delete the issue.
</div>
)}
</StrictModeDroppable>

View File

@ -74,7 +74,7 @@ export const IssueAttachmentUpload = () => {
onDrop,
maxSize: maxFileSize,
multiple: false,
disabled: isLoading
disabled: isLoading,
});
const fileError =
@ -85,8 +85,8 @@ export const IssueAttachmentUpload = () => {
return (
<div
{...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 ${
isDragActive ? "bg-theme/10" : ""
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-brand-accent/10 border-brand-accent" : "border-brand-base"
} ${isDragReject ? "bg-red-100" : ""}`}
>
<input {...getInputProps()} />

View File

@ -450,7 +450,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100"
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">
{issueLabels ? (
issueLabels.length > 0 ? (
@ -468,8 +468,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
`${
active || selected ? "bg-brand-surface-1" : ""
} ${
selected ? "font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-brand-base`
selected ? "" : "text-brand-secondary"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={label.id}
>
@ -489,7 +489,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
return (
<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">
<RectangleGroupIcon className="h-3 w-3" />{" "}
<RectangleGroupIcon className="h-3 w-3" />
{label.name}
</div>
<div>
@ -497,9 +497,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<Listbox.Option
key={child.id}
className={({ active, selected }) =>
`${active || selected ? "bg-indigo-50" : ""} ${
selected ? "font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-brand-base`
`${active || selected ? "bg-brand-base" : ""} ${
selected ? "" : "text-brand-secondary"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={child.id}
>