style: kanban board

This commit is contained in:
Aaryan Khandelwal 2023-02-28 14:42:46 +05:30
parent 1b369feb6a
commit 0cd3bb5956
11 changed files with 200 additions and 68 deletions

View File

@ -40,8 +40,13 @@ export const AllBoards: React.FC<Props> = ({
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full"> <div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<div className="h-full w-full"> <div className="h-full w-full">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden"> <div className="flex h-full gap-x-12 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => { {Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)
: null;
const stateId = const stateId =
selectedGroup === "state_detail.name" selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null ? states?.find((s) => s.name === singleGroup)?.id ?? null
@ -56,6 +61,7 @@ export const AllBoards: React.FC<Props> = ({
<SingleBoard <SingleBoard
key={index} key={index}
type={type} type={type}
currentState={currentState}
bgColor={bgColor} bgColor={bgColor}
groupTitle={singleGroup} groupTitle={singleGroup}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}

View File

@ -1,22 +1,17 @@
import React from "react"; import React from "react";
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
// icons // icons
import { import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import { IIssue, IProjectMember, NestedKeyOf } from "types"; import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
import { getStateGroupIcon } from "components/icons";
type Props = { type Props = {
groupedByIssues: { groupedByIssues: {
[key: string]: IIssue[]; [key: string]: IIssue[];
}; };
currentState?: IState | null;
selectedGroup: NestedKeyOf<IIssue> | null; selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string; groupTitle: string;
bgColor?: string; bgColor?: string;
@ -28,6 +23,7 @@ type Props = {
export const BoardHeader: React.FC<Props> = ({ export const BoardHeader: React.FC<Props> = ({
groupedByIssues, groupedByIssues,
currentState,
selectedGroup, selectedGroup,
groupTitle, groupTitle,
bgColor, bgColor,
@ -60,16 +56,13 @@ export const BoardHeader: React.FC<Props> = ({
> >
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div <div
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${ className={`flex cursor-pointer items-center gap-x-3.5 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : "" !isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`} }`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
> >
{currentState && getStateGroupIcon(currentState.group)}
<h2 <h2
className={`text-[0.9rem] font-medium capitalize`} className={`text-xl font-semibold capitalize`}
style={{ style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}} }}
@ -80,14 +73,16 @@ export const BoardHeader: React.FC<Props> = ({
? assignees ? assignees
: addSpaceIfCamelCase(groupTitle)} : addSpaceIfCamelCase(groupTitle)}
</h2> </h2>
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span> <span className="ml-0.5 text-sm bg-gray-100 py-1 px-3 rounded-full">
{groupedByIssues[groupTitle].length}
</span>
</div> </div>
</div> </div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200" className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-200"
onClick={() => { onClick={() => {
setIsCollapsed((prevData) => !prevData); setIsCollapsed((prevData) => !prevData);
}} }}
@ -100,7 +95,7 @@ export const BoardHeader: React.FC<Props> = ({
</button> </button>
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200" className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-200"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />

View File

@ -14,10 +14,11 @@ import { CustomMenu } from "components/ui";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types"; import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
type Props = { type Props = {
type?: "issue" | "cycle" | "module"; type?: "issue" | "cycle" | "module";
currentState?: IState | null;
bgColor?: string; bgColor?: string;
groupTitle: string; groupTitle: string;
groupedByIssues: { groupedByIssues: {
@ -37,6 +38,7 @@ type Props = {
export const SingleBoard: React.FC<Props> = ({ export const SingleBoard: React.FC<Props> = ({
type, type,
currentState,
bgColor, bgColor,
groupTitle, groupTitle,
groupedByIssues, groupedByIssues,
@ -71,10 +73,11 @@ export const SingleBoard: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( return (
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}> <div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}> <div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader <BoardHeader
addIssueToState={addIssueToState} addIssueToState={addIssueToState}
currentState={currentState}
bgColor={bgColor} bgColor={bgColor}
selectedGroup={selectedGroup} selectedGroup={selectedGroup}
groupTitle={groupTitle} groupTitle={groupTitle}

View File

@ -184,7 +184,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
return ( return (
<div <div
className={`rounded border bg-white shadow-sm mb-3 ${ className={`rounded bg-white shadow mb-3 ${
snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : "" snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
}`} }`}
ref={provided.innerRef} ref={provided.innerRef}
@ -192,7 +192,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{...provided.dragHandleProps} {...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)} style={getStyle(provided.draggableProps.style, snapshot)}
> >
<div className="group/card relative select-none p-2"> <div className="group/card relative select-none p-4">
{!isNotAllowed && ( {!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100"> <div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
{type && !isNotAllowed && ( {type && !isNotAllowed && (
@ -214,19 +214,19 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a> <a>
{properties.key && ( {properties.key && (
<div className="mb-2 text-xs font-medium text-gray-500"> <div className="mb-2.5 text-xs font-medium text-gray-700">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
)} )}
<h5 <h5
className="mb-3 text-sm group-hover:text-theme" className="text-sm group-hover:text-theme"
style={{ lineClamp: 3, WebkitLineClamp: 3 }} style={{ lineClamp: 3, WebkitLineClamp: 3 }}
> >
{issue.name} {issue.name}
</h5> </h5>
</a> </a>
</Link> </Link>
<div className="relative flex flex-wrap items-center gap-x-1 gap-y-2 text-xs"> <div className="relative flex flex-wrap items-center gap-2 mt-2.5 text-xs">
{properties.priority && selectedGroup !== "priority" && ( {properties.priority && selectedGroup !== "priority" && (
<ViewPrioritySelect <ViewPrioritySelect
issue={issue} issue={issue}

View File

@ -0,0 +1,21 @@
import React from "react";
import type { Props } from "./types";
export const BacklogStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "black",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="10" cy="10" r="9" stroke={color} strokeLinecap="round" strokeDasharray="4 4" />
</svg>
);

View File

@ -0,0 +1,33 @@
import React from "react";
import type { Props } from "./types";
export const CompletedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#438af3",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="10"
cy="10"
r="9"
stroke={color}
strokeLinecap="round"
strokeDasharray="0 20 0 10"
/>
<circle cx="10" cy="10" r="7" fill={color} />
<path
d="M13 8.33328L9 12.3333L7.16666 10.4999L7.63666 10.0299L9 11.3899L12.53 7.86328L13 8.33328Z"
fill="white"
/>
</svg>
);

View File

@ -1,4 +1,5 @@
export * from "./attachment-icon"; export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon"; export * from "./blocked-icon";
export * from "./blocker-icon"; export * from "./blocker-icon";
export * from "./bolt-icon"; export * from "./bolt-icon";
@ -7,6 +8,7 @@ export * from "./cancel-icon";
export * from "./clipboard-icon"; export * from "./clipboard-icon";
export * from "./comment-icon"; export * from "./comment-icon";
export * from "./completed-cycle-icon"; export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon"; export * from "./current-cycle-icon";
export * from "./cycle-icon"; export * from "./cycle-icon";
export * from "./discord-icon"; export * from "./discord-icon";
@ -16,6 +18,7 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon"; export * from "./external-link-icon";
export * from "./github-icon"; export * from "./github-icon";
export * from "./heartbeat-icon"; export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon"; export * from "./layer-diagonal-icon";
export * from "./lock-icon"; export * from "./lock-icon";
export * from "./menu-icon"; export * from "./menu-icon";
@ -23,6 +26,8 @@ export * from "./plus-icon";
export * from "./question-mark-circle-icon"; export * from "./question-mark-circle-icon";
export * from "./setting-icon"; export * from "./setting-icon";
export * from "./signal-cellular-icon"; export * from "./signal-cellular-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon"; export * from "./tag-icon";
export * from "./tune-icon"; export * from "./tune-icon";
export * from "./upcoming-cycle-icon"; export * from "./upcoming-cycle-icon";

View File

@ -0,0 +1,36 @@
import React from "react";
import type { Props } from "./types";
export const StartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#fcbe1d",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="10"
cy="10"
r="9"
stroke={color}
strokeLinecap="round"
strokeDasharray="0 20 0 10"
/>
<path
d="M14.2878 4.46695C13.0513 3.5087 11.5294 2.99227 9.96503 3.00009C8.40068 3.0079 6.88403 3.53951 5.65713 4.51006L10 10L14.2878 4.46695Z"
fill={color}
/>
<path
d="M5.70047 15.5331C6.93701 16.4913 8.45889 17.0077 10.0232 16.9999C11.5876 16.9921 13.1043 16.4605 14.3312 15.4899L9.98828 10L5.70047 15.5331Z"
fill={color}
/>
</svg>
);

View File

@ -0,0 +1,25 @@
import { BacklogStateIcon } from "./backlog-state-icon";
import { CompletedStateIcon } from "./completed-state-icon";
import { StartedStateIcon } from "./started-state-icon";
export const getStateGroupIcon = (
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
width = "20",
height = "20",
color?: string
) => {
switch (stateGroup) {
case "backlog":
return <BacklogStateIcon width={width} height={height} color={color} />;
case "unstarted":
return <StartedStateIcon width={width} height={height} color={color} />;
case "started":
return <StartedStateIcon width={width} height={height} color={color} />;
case "completed":
return <CompletedStateIcon width={width} height={height} color={color} />;
case "cancelled":
return <StartedStateIcon width={width} height={height} color={color} />;
default:
return <></>;
}
};

View File

@ -23,34 +23,38 @@ export const ViewPrioritySelect: React.FC<Props> = ({
isNotAllowed, isNotAllowed,
}) => ( }) => (
<CustomSelect <CustomSelect
label={
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
}
value={issue.state} value={issue.state}
onChange={(data: string) => { onChange={(data: string) => {
partialUpdateIssue({ priority: data }); partialUpdateIssue({ priority: data });
}} }}
maxHeight="md" maxHeight="md"
buttonClassName={`flex ${ customButton={
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" <button
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${ type="button"
issue.priority === "urgent" className={`grid place-items-center rounded w-6 h-6 ${
? "bg-red-100 text-red-600 hover:bg-red-100" isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
: issue.priority === "high" } items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
? "bg-orange-100 text-orange-500 hover:bg-orange-100" issue.priority === "urgent"
: issue.priority === "medium" ? "bg-red-100 text-red-600 hover:bg-red-100"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100" : issue.priority === "high"
: issue.priority === "low" ? "bg-orange-100 text-orange-500 hover:bg-orange-100"
? "bg-green-100 text-green-500 hover:bg-green-100" : issue.priority === "medium"
: "bg-gray-100" ? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
} border-none`} : issue.priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100"
} border-none`}
>
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
</button>
}
noChevron noChevron
disabled={isNotAllowed} disabled={isNotAllowed}
selfPositioned={selfPositioned} selfPositioned={selfPositioned}

View File

@ -8,13 +8,13 @@ type CustomSelectProps = {
value: any; value: any;
onChange: any; onChange: any;
children: React.ReactNode; children: React.ReactNode;
label: string | JSX.Element; label?: string | JSX.Element;
textAlignment?: "left" | "center" | "right"; textAlignment?: "left" | "center" | "right";
maxHeight?: "sm" | "rg" | "md" | "lg" | "none"; maxHeight?: "sm" | "rg" | "md" | "lg" | "none";
width?: "auto" | string; width?: "auto" | string;
input?: boolean; input?: boolean;
noChevron?: boolean; noChevron?: boolean;
buttonClassName?: string; customButton?: JSX.Element;
optionsClassName?: string; optionsClassName?: string;
disabled?: boolean; disabled?: boolean;
selfPositioned?: boolean; selfPositioned?: boolean;
@ -30,7 +30,7 @@ const CustomSelect = ({
width = "auto", width = "auto",
input = false, input = false,
noChevron = false, noChevron = false,
buttonClassName = "", customButton,
optionsClassName = "", optionsClassName = "",
disabled = false, disabled = false,
selfPositioned = false, selfPositioned = false,
@ -43,22 +43,26 @@ const CustomSelect = ({
disabled={disabled} disabled={disabled}
> >
<div> <div>
<Listbox.Button {customButton ? (
className={`${buttonClassName} flex w-full ${ customButton
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100" ) : (
} items-center justify-between gap-1 rounded-md border shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${ <Listbox.Button
input ? "border-gray-300 px-3 py-2 text-sm" : "px-2 py-1 text-xs" className={`flex w-full ${
} ${ disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100"
textAlignment === "right" } items-center justify-between gap-1 rounded-md border shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
? "text-right" input ? "border-gray-300 px-3 py-2 text-sm" : "px-2 py-1 text-xs"
: textAlignment === "center" } ${
? "text-center" textAlignment === "right"
: "text-left" ? "text-right"
}`} : textAlignment === "center"
> ? "text-center"
{label} : "text-left"
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />} }`}
</Listbox.Button> >
{label}
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</Listbox.Button>
)}
</div> </div>
<Transition <Transition