feat: dashboard widgets (#3362)

* 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>
This commit is contained in:
Bavisetti Narayan 2024-01-18 15:49:54 +05:30 committed by sriram veeraghanta
parent f347c1cd69
commit c9337d4a41
122 changed files with 6790 additions and 849 deletions

4
packages/ui/helpers.ts Normal file
View file

@ -0,0 +1,4 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

View file

@ -17,6 +17,17 @@
"lint": "eslint src/",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"dependencies": {
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@types/node": "^20.5.2",
"@types/react": "^18.2.42",
@ -29,14 +40,5 @@
"tsconfig": "*",
"tsup": "^5.10.1",
"typescript": "4.7.4"
},
"dependencies": {
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0"
}
}

View file

@ -141,6 +141,7 @@ export const Avatar: React.FC<Props> = (props) => {
}
: {}
}
tabIndex={-1}
>
{src ? (
<img src={src} className={`h-full w-full ${getBorderRadius(shape)} ${className}`} alt={name} />

View file

@ -1,6 +1,7 @@
import * as React from "react";
import { getIconStyling, getButtonStyling, TButtonVariant, TButtonSizes } from "./helper";
import { cn } from "../../helpers";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: TButtonVariant;
@ -31,7 +32,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) =>
const buttonIconStyle = getIconStyling(size);
return (
<button ref={ref} type={type} className={`${buttonStyle} ${className}`} disabled={disabled || loading} {...rest}>
<button ref={ref} type={type} className={cn(buttonStyle, className)} disabled={disabled || loading} {...rest}>
{prependIcon && <div className={buttonIconStyle}>{React.cloneElement(prependIcon, { strokeWidth: 2 })}</div>}
{children}
{appendIcon && <div className={buttonIconStyle}>{React.cloneElement(appendIcon, { strokeWidth: 2 })}</div>}

View file

@ -22,10 +22,10 @@ export interface IButtonStyling {
}
enum buttonSizeStyling {
sm = `px-3 py-1.5 font-medium text-xs rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
md = `px-4 py-1.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
lg = `px-5 py-2 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
xl = `px-5 py-3.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
sm = `px-3 py-1.5 font-medium text-xs rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
md = `px-4 py-1.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
lg = `px-5 py-2 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
xl = `px-5 py-3.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
}
enum buttonIconStyling {

View file

@ -1,2 +1,3 @@
export * from "./button";
export * from "./helper";
export * from "./toggle-switch";

View file

@ -11,6 +11,7 @@ import { Menu } from "@headlessui/react";
import { ICustomMenuDropdownProps, ICustomMenuItemProps } from "./helper";
// icons
import { ChevronDown, MoreHorizontal } from "lucide-react";
import { cn } from "../../helpers";
const CustomMenu = (props: ICustomMenuDropdownProps) => {
const {
@ -62,7 +63,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
static
>
<div
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
className={`my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none ${
maxHeight === "lg"
? "max-h-60"
: maxHeight === "md"
@ -72,7 +73,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
: maxHeight === "sm"
? "max-h-28"
: ""
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
} ${width === "auto" ? "min-w-[12rem] whitespace-nowrap" : width} ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
@ -167,9 +168,13 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
{({ active, close }) => (
<button
type="button"
className={`w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80 ${
active ? "bg-custom-background-80" : ""
} ${className}`}
className={cn(
"w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200",
{
"bg-custom-background-80": active,
},
className
)}
onClick={(e) => {
close();
onClick && onClick(e);

View file

@ -1,35 +1,79 @@
import * as React from "react";
import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react";
import { cn } from "../../helpers";
type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
interface IPriorityIcon {
className?: string;
containerClassName?: string;
priority: TIssuePriorities;
size?: number;
withContainer?: boolean;
}
export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
const { priority, className = "", size = 14 } = props;
const { priority, className = "", containerClassName = "", size = 14, withContainer = false } = props;
// Convert to lowercase for string comparison
const lowercasePriority = priority?.toLowerCase();
//get priority icon
const getPriorityIcon = (): React.ReactNode => {
switch (lowercasePriority) {
case "urgent":
return <AlertCircle size={size} className={`text-red-500 ${className}`} />;
case "high":
return <SignalHigh size={size} strokeWidth={3} className={`text-orange-500 ${className}`} />;
case "medium":
return <SignalMedium size={size} strokeWidth={3} className={`text-yellow-500 ${className}`} />;
case "low":
return <SignalLow size={size} strokeWidth={3} className={`text-custom-primary-100 ${className}`} />;
default:
return <Ban size={size} className={`text-custom-text-200 ${className}`} />;
}
const priorityClasses = {
urgent: "bg-red-500 text-red-500 border-red-500",
high: "bg-orange-500/20 text-orange-500 border-orange-500",
medium: "bg-yellow-500/20 text-yellow-500 border-yellow-500",
low: "bg-custom-primary-100/20 text-custom-primary-100 border-custom-primary-100",
none: "bg-custom-background-80 text-custom-text-200 border-custom-border-300",
};
return <>{getPriorityIcon()}</>;
// get priority icon
const icons = {
urgent: AlertCircle,
high: SignalHigh,
medium: SignalMedium,
low: SignalLow,
none: Ban,
};
const Icon = icons[priority];
if (!Icon) return null;
return (
<>
{withContainer ? (
<div
className={cn(
"grid place-items-center border rounded p-0.5 flex-shrink-0",
priorityClasses[priority],
containerClassName
)}
>
<Icon
size={size}
className={cn(
{
"text-white": priority === "urgent",
// centre align the icons
"translate-x-[0.0625rem]": priority === "high",
"translate-x-0.5": priority === "medium",
"translate-x-1": priority === "low",
},
className
)}
/>
</div>
) : (
<Icon
size={size}
className={cn(
{
"text-red-500": priority === "urgent",
"text-orange-500": priority === "high",
"text-yellow-500": priority === "medium",
"text-custom-primary-100": priority === "low",
"text-custom-text-200": priority === "none",
},
className
)}
/>
)}
</>
);
};