forked from github/plane
style: kanban board
This commit is contained in:
parent
1b369feb6a
commit
0cd3bb5956
@ -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-full w-full overflow-hidden">
|
||||
<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) => {
|
||||
const currentState =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)
|
||||
: null;
|
||||
|
||||
const stateId =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
@ -56,6 +61,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
<SingleBoard
|
||||
key={index}
|
||||
type={type}
|
||||
currentState={currentState}
|
||||
bgColor={bgColor}
|
||||
groupTitle={singleGroup}
|
||||
groupedByIssues={groupedByIssues}
|
||||
|
@ -1,22 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DraggableProvided } from "react-beautiful-dnd";
|
||||
// icons
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IProjectMember, NestedKeyOf } from "types";
|
||||
import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
type Props = {
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
currentState?: IState | null;
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
groupTitle: string;
|
||||
bgColor?: string;
|
||||
@ -28,6 +23,7 @@ type Props = {
|
||||
|
||||
export const BoardHeader: React.FC<Props> = ({
|
||||
groupedByIssues,
|
||||
currentState,
|
||||
selectedGroup,
|
||||
groupTitle,
|
||||
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 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" : ""
|
||||
}`}
|
||||
style={{
|
||||
border: `2px solid ${bgColor}`,
|
||||
backgroundColor: `${bgColor}20`,
|
||||
}}
|
||||
>
|
||||
{currentState && getStateGroupIcon(currentState.group)}
|
||||
<h2
|
||||
className={`text-[0.9rem] font-medium capitalize`}
|
||||
className={`text-xl font-semibold capitalize`}
|
||||
style={{
|
||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
||||
}}
|
||||
@ -80,14 +73,16 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
? assignees
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
</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 className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||
<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={() => {
|
||||
setIsCollapsed((prevData) => !prevData);
|
||||
}}
|
||||
@ -100,7 +95,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
</button>
|
||||
<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}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
|
@ -14,10 +14,11 @@ import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
|
||||
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
type?: "issue" | "cycle" | "module";
|
||||
currentState?: IState | null;
|
||||
bgColor?: string;
|
||||
groupTitle: string;
|
||||
groupedByIssues: {
|
||||
@ -37,6 +38,7 @@ type Props = {
|
||||
|
||||
export const SingleBoard: React.FC<Props> = ({
|
||||
type,
|
||||
currentState,
|
||||
bgColor,
|
||||
groupTitle,
|
||||
groupedByIssues,
|
||||
@ -71,10 +73,11 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
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"}`}>
|
||||
<BoardHeader
|
||||
addIssueToState={addIssueToState}
|
||||
currentState={currentState}
|
||||
bgColor={bgColor}
|
||||
selectedGroup={selectedGroup}
|
||||
groupTitle={groupTitle}
|
||||
|
@ -184,7 +184,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<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" : ""
|
||||
}`}
|
||||
ref={provided.innerRef}
|
||||
@ -192,7 +192,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{...provided.dragHandleProps}
|
||||
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 && (
|
||||
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
|
||||
{type && !isNotAllowed && (
|
||||
@ -214,19 +214,19 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
<h5
|
||||
className="mb-3 text-sm group-hover:text-theme"
|
||||
className="text-sm group-hover:text-theme"
|
||||
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
||||
>
|
||||
{issue.name}
|
||||
</h5>
|
||||
</a>
|
||||
</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" && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
|
21
apps/app/components/icons/backlog-state-icon.tsx
Normal file
21
apps/app/components/icons/backlog-state-icon.tsx
Normal 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>
|
||||
);
|
33
apps/app/components/icons/completed-state-icon.tsx
Normal file
33
apps/app/components/icons/completed-state-icon.tsx
Normal 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>
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
export * from "./attachment-icon";
|
||||
export * from "./backlog-state-icon";
|
||||
export * from "./blocked-icon";
|
||||
export * from "./blocker-icon";
|
||||
export * from "./bolt-icon";
|
||||
@ -7,6 +8,7 @@ export * from "./cancel-icon";
|
||||
export * from "./clipboard-icon";
|
||||
export * from "./comment-icon";
|
||||
export * from "./completed-cycle-icon";
|
||||
export * from "./completed-state-icon";
|
||||
export * from "./current-cycle-icon";
|
||||
export * from "./cycle-icon";
|
||||
export * from "./discord-icon";
|
||||
@ -16,6 +18,7 @@ export * from "./ellipsis-horizontal-icon";
|
||||
export * from "./external-link-icon";
|
||||
export * from "./github-icon";
|
||||
export * from "./heartbeat-icon";
|
||||
export * from "./started-state-icon";
|
||||
export * from "./layer-diagonal-icon";
|
||||
export * from "./lock-icon";
|
||||
export * from "./menu-icon";
|
||||
@ -23,6 +26,8 @@ export * from "./plus-icon";
|
||||
export * from "./question-mark-circle-icon";
|
||||
export * from "./setting-icon";
|
||||
export * from "./signal-cellular-icon";
|
||||
export * from "./started-state-icon";
|
||||
export * from "./state-group-icon";
|
||||
export * from "./tag-icon";
|
||||
export * from "./tune-icon";
|
||||
export * from "./upcoming-cycle-icon";
|
||||
|
36
apps/app/components/icons/started-state-icon.tsx
Normal file
36
apps/app/components/icons/started-state-icon.tsx
Normal 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>
|
||||
);
|
25
apps/app/components/icons/state-group-icon.tsx
Normal file
25
apps/app/components/icons/state-group-icon.tsx
Normal 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 <></>;
|
||||
}
|
||||
};
|
@ -23,34 +23,38 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<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}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data });
|
||||
}}
|
||||
maxHeight="md"
|
||||
buttonClassName={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} 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 ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600 hover:bg-red-100"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500 hover:bg-green-100"
|
||||
: "bg-gray-100"
|
||||
} border-none`}
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded w-6 h-6 ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600 hover:bg-red-100"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
|
||||
: 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
|
||||
disabled={isNotAllowed}
|
||||
selfPositioned={selfPositioned}
|
||||
|
@ -8,13 +8,13 @@ type CustomSelectProps = {
|
||||
value: any;
|
||||
onChange: any;
|
||||
children: React.ReactNode;
|
||||
label: string | JSX.Element;
|
||||
label?: string | JSX.Element;
|
||||
textAlignment?: "left" | "center" | "right";
|
||||
maxHeight?: "sm" | "rg" | "md" | "lg" | "none";
|
||||
width?: "auto" | string;
|
||||
input?: boolean;
|
||||
noChevron?: boolean;
|
||||
buttonClassName?: string;
|
||||
customButton?: JSX.Element;
|
||||
optionsClassName?: string;
|
||||
disabled?: boolean;
|
||||
selfPositioned?: boolean;
|
||||
@ -30,7 +30,7 @@ const CustomSelect = ({
|
||||
width = "auto",
|
||||
input = false,
|
||||
noChevron = false,
|
||||
buttonClassName = "",
|
||||
customButton,
|
||||
optionsClassName = "",
|
||||
disabled = false,
|
||||
selfPositioned = false,
|
||||
@ -43,22 +43,26 @@ const CustomSelect = ({
|
||||
disabled={disabled}
|
||||
>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className={`${buttonClassName} flex w-full ${
|
||||
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 ${
|
||||
input ? "border-gray-300 px-3 py-2 text-sm" : "px-2 py-1 text-xs"
|
||||
} ${
|
||||
textAlignment === "right"
|
||||
? "text-right"
|
||||
: textAlignment === "center"
|
||||
? "text-center"
|
||||
: "text-left"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
</Listbox.Button>
|
||||
{customButton ? (
|
||||
customButton
|
||||
) : (
|
||||
<Listbox.Button
|
||||
className={`flex w-full ${
|
||||
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 ${
|
||||
input ? "border-gray-300 px-3 py-2 text-sm" : "px-2 py-1 text-xs"
|
||||
} ${
|
||||
textAlignment === "right"
|
||||
? "text-right"
|
||||
: textAlignment === "center"
|
||||
? "text-center"
|
||||
: "text-left"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
</Listbox.Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
|
Loading…
Reference in New Issue
Block a user