mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: bug fixes in cycles
This commit is contained in:
parent
1c30e53ef9
commit
0cda15408d
@ -1,662 +0,0 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import cycleServices from "lib/services/cycles.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// icons
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
CalendarDaysIcon,
|
||||
PlusIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import {
|
||||
CycleIssueResponse,
|
||||
ICycle,
|
||||
IIssue,
|
||||
IWorkspaceMember,
|
||||
NestedKeyOf,
|
||||
Properties,
|
||||
} from "types";
|
||||
// constants
|
||||
import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
|
||||
type Props = {
|
||||
properties: Properties;
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
groupTitle: string;
|
||||
createdBy: string | null;
|
||||
bgColor?: string;
|
||||
openCreateIssueModal: (
|
||||
sprintId: string,
|
||||
issue?: IIssue,
|
||||
actionType?: "create" | "edit" | "delete"
|
||||
) => void;
|
||||
openIssuesListModal: (cycleId: string) => void;
|
||||
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
|
||||
};
|
||||
|
||||
const SingleCycleBoard: React.FC<Props> = ({
|
||||
properties,
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
groupTitle,
|
||||
createdBy,
|
||||
bgColor,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
removeIssueFromCycle,
|
||||
}) => {
|
||||
// Collapse/Expand
|
||||
const [show, setState] = useState(true);
|
||||
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
if (selectedGroup === "priority")
|
||||
groupTitle === "high"
|
||||
? (bgColor = "#dc2626")
|
||||
: groupTitle === "medium"
|
||||
? (bgColor = "#f97316")
|
||||
: groupTitle === "low"
|
||||
? (bgColor = "#22c55e")
|
||||
: (bgColor = "#ff0000");
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`rounded flex-shrink-0 h-full ${!show ? "" : "w-80 bg-gray-50 border"}`}>
|
||||
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
|
||||
<div
|
||||
className={`flex justify-between p-3 pb-0 ${
|
||||
!show ? "flex-col bg-gray-50 rounded-md border" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
|
||||
<div
|
||||
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
|
||||
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
|
||||
}`}
|
||||
style={{
|
||||
border: `2px solid ${bgColor}`,
|
||||
backgroundColor: `${bgColor}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
|
||||
style={{
|
||||
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
|
||||
}}
|
||||
/>
|
||||
<h2
|
||||
className={`text-[0.9rem] font-medium capitalize`}
|
||||
style={{
|
||||
writingMode: !show ? "vertical-rl" : "horizontal-tb",
|
||||
}}
|
||||
>
|
||||
{groupTitle === null || groupTitle === "null"
|
||||
? "None"
|
||||
: createdBy
|
||||
? createdBy
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
</h2>
|
||||
<span className="text-gray-500 text-sm ml-0.5">
|
||||
{groupedByIssues[groupTitle].length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
|
||||
!show ? "hidden" : "block"
|
||||
}`}
|
||||
>
|
||||
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
|
||||
const assignees = [
|
||||
...(childIssue?.assignees_list ?? []),
|
||||
...(childIssue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
|
||||
|
||||
return {
|
||||
avatar: tempPerson?.avatar,
|
||||
first_name: tempPerson?.first_name,
|
||||
email: tempPerson?.email,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={childIssue.id} className={`border rounded bg-white shadow-sm`}>
|
||||
<div className="group/card relative p-2 select-none">
|
||||
<div className="opacity-0 group-hover/card:opacity-100 absolute top-1 right-1">
|
||||
<button
|
||||
type="button"
|
||||
className="h-7 w-7 p-1 grid place-items-center rounded text-red-500 hover:bg-red-50 duration-300 outline-none"
|
||||
// onClick={() => handleDeleteIssue(childIssue.id)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
|
||||
<a>
|
||||
{properties.key && (
|
||||
<div className="text-xs font-medium text-gray-500 mb-2">
|
||||
{activeProject?.identifier}-{childIssue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
<h5
|
||||
className="group-hover:text-theme text-sm break-all mb-3"
|
||||
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
||||
>
|
||||
{childIssue.name}
|
||||
</h5>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
{properties.priority && (
|
||||
<div
|
||||
className={`group flex-shrink-0 flex items-center gap-1 rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
childIssue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: childIssue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: childIssue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: childIssue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(childIssue.priority ?? "")} */}
|
||||
{childIssue.priority ?? "None"}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||
<div
|
||||
className={`capitalize ${
|
||||
childIssue.priority === "urgent"
|
||||
? "text-red-600"
|
||||
: childIssue.priority === "high"
|
||||
? "text-orange-500"
|
||||
: childIssue.priority === "medium"
|
||||
? "text-yellow-500"
|
||||
: childIssue.priority === "low"
|
||||
? "text-green-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{childIssue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.state && (
|
||||
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{ backgroundColor: childIssue.state_detail.color }}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(childIssue.state_detail.name)}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">State</h5>
|
||||
<div>{childIssue.state_detail.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.start_date && (
|
||||
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.start_date
|
||||
? renderShortNumericDateFormat(childIssue.start_date)
|
||||
: "N/A"}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Started at</h5>
|
||||
<div>{renderShortNumericDateFormat(childIssue.start_date ?? "")}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.target_date && (
|
||||
<div
|
||||
className={`group flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
childIssue.target_date === null
|
||||
? ""
|
||||
: childIssue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.target_date
|
||||
? renderShortNumericDateFormat(childIssue.target_date)
|
||||
: "N/A"}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Target date</h5>
|
||||
<div>{renderShortNumericDateFormat(childIssue.target_date ?? "")}</div>
|
||||
<div>
|
||||
{childIssue.target_date &&
|
||||
(childIssue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: "Target date")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="group flex items-center gap-1 text-xs">
|
||||
{childIssue.assignee_details?.length > 0 ? (
|
||||
childIssue.assignee_details?.map((assignee, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
{assignee.avatar && assignee.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={assignee.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={assignee.name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{assignee.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Assigned to</h5>
|
||||
<div>
|
||||
{childIssue.assignee_details?.length > 0
|
||||
? childIssue.assignee_details
|
||||
.map((assignee) => assignee.first_name)
|
||||
.join(", ")
|
||||
: "No one"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3 mr-1" />
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <div className={`rounded flex-shrink-0 h-full ${!show ? "" : "w-80 bg-gray-50 border"}`}>
|
||||
// <div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
|
||||
// <div
|
||||
// className={`flex justify-between p-3 pb-0 ${
|
||||
// !show ? "flex-col bg-gray-50 rounded-md border" : ""
|
||||
// }`}
|
||||
// >
|
||||
// <div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
|
||||
// <div
|
||||
// className={`flex items-center gap-x-1 rounded-md cursor-pointer ${
|
||||
// !show ? "py-2 mb-2 flex-col gap-y-2" : ""
|
||||
// }`}
|
||||
// >
|
||||
// <h2
|
||||
// className={`text-[0.9rem] font-medium capitalize`}
|
||||
// style={{
|
||||
// writingMode: !show ? "vertical-rl" : "horizontal-tb",
|
||||
// }}
|
||||
// >
|
||||
// {cycle.name}
|
||||
// </h2>
|
||||
// <span className="text-gray-500 text-sm ml-0.5">{cycleIssues?.length}</span>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className={`flex items-center ${!show ? "flex-col pb-2" : ""}`}>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
||||
// onClick={() => {
|
||||
// setState(!show);
|
||||
// }}
|
||||
// >
|
||||
// {show ? (
|
||||
// <ArrowsPointingInIcon className="h-4 w-4" />
|
||||
// ) : (
|
||||
// <ArrowsPointingOutIcon className="h-4 w-4" />
|
||||
// )}
|
||||
// </button>
|
||||
// <Menu as="div" className="relative inline-block">
|
||||
// <Menu.Button className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none">
|
||||
// <EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
// </Menu.Button>
|
||||
|
||||
// <Transition
|
||||
// as={React.Fragment}
|
||||
// enter="transition ease-out duration-100"
|
||||
// enterFrom="transform opacity-0 scale-95"
|
||||
// enterTo="transform opacity-100 scale-100"
|
||||
// leave="transition ease-in duration-75"
|
||||
// leaveFrom="transform opacity-100 scale-100"
|
||||
// leaveTo="transform opacity-0 scale-95"
|
||||
// >
|
||||
// <Menu.Items className="absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
// <div className="p-1">
|
||||
// <Menu.Item as="div">
|
||||
// {(active) => (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// onClick={() => openCreateIssueModal(cycle.id)}
|
||||
// >
|
||||
// Create new
|
||||
// </button>
|
||||
// )}
|
||||
// </Menu.Item>
|
||||
// <Menu.Item as="div">
|
||||
// {(active) => (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||
// onClick={() => openIssuesListModal(cycle.id)}
|
||||
// >
|
||||
// Add an existing issue
|
||||
// </button>
|
||||
// )}
|
||||
// </Menu.Item>
|
||||
// </div>
|
||||
// </Menu.Items>
|
||||
// </Transition>
|
||||
// </Menu>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div
|
||||
// className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
|
||||
// !show ? "hidden" : "block"
|
||||
// }`}
|
||||
// >
|
||||
// {cycleIssues ? (
|
||||
// cycleIssues.map((issue, index: number) => (
|
||||
// <div key={childIssue.id} className="border rounded bg-white shadow-sm">
|
||||
// <div className="group/card relative p-2 select-none">
|
||||
// <div className="opacity-0 group-hover/card:opacity-100 absolute top-1 right-1">
|
||||
// <Menu as="div" className="relative">
|
||||
// <Menu.Button
|
||||
// as="button"
|
||||
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||
// >
|
||||
// <EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
// </Menu.Button>
|
||||
// <Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
// <Menu.Item>
|
||||
// <div className="hover:bg-gray-100 border-b last:border-0">
|
||||
// <button
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// type="button"
|
||||
// onClick={() => removeIssueFromCycle(issue.cycle, issue.id)}
|
||||
// >
|
||||
// Remove from cycle
|
||||
// </button>
|
||||
// </div>
|
||||
// </Menu.Item>
|
||||
// <Menu.Item>
|
||||
// <div className="hover:bg-gray-100 border-b last:border-0">
|
||||
// <button
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// type="button"
|
||||
// onClick={() =>
|
||||
// openCreateIssueModal(cycle.id, childIssue, "delete")
|
||||
// }
|
||||
// >
|
||||
// Delete permanently
|
||||
// </button>
|
||||
// </div>
|
||||
// </Menu.Item>
|
||||
// </Menu.Items>
|
||||
// </Menu>
|
||||
// </div>
|
||||
// <Link
|
||||
// href={`/projects/${childIssue.project}/issues/${childIssue.id}`}
|
||||
// >
|
||||
// <a>
|
||||
// {properties.key && (
|
||||
// <div className="text-xs font-medium text-gray-500 mb-2">
|
||||
// {activeProject?.identifier}-{childIssue.sequence_id}
|
||||
// </div>
|
||||
// )}
|
||||
// <h5
|
||||
// className="group-hover:text-theme text-sm break-all mb-3"
|
||||
// style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
||||
// >
|
||||
// {childIssue.name}
|
||||
// </h5>
|
||||
// </a>
|
||||
// </Link>
|
||||
// <div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
// {properties.priority && (
|
||||
// <div
|
||||
// className={`group flex-shrink-0 flex items-center gap-1 rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
// childIssue.priority === "urgent"
|
||||
// ? "bg-red-100 text-red-600"
|
||||
// : childIssue.priority === "high"
|
||||
// ? "bg-orange-100 text-orange-500"
|
||||
// : childIssue.priority === "medium"
|
||||
// ? "bg-yellow-100 text-yellow-500"
|
||||
// : childIssue.priority === "low"
|
||||
// ? "bg-green-100 text-green-500"
|
||||
// : "bg-gray-100"
|
||||
// }`}
|
||||
// >
|
||||
// {/* {getPriorityIcon(childIssue.priority ?? "")} */}
|
||||
// {childIssue.priority ?? "None"}
|
||||
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||
// <div
|
||||
// className={`capitalize ${
|
||||
// childIssue.priority === "urgent"
|
||||
// ? "text-red-600"
|
||||
// : childIssue.priority === "high"
|
||||
// ? "text-orange-500"
|
||||
// : childIssue.priority === "medium"
|
||||
// ? "text-yellow-500"
|
||||
// : childIssue.priority === "low"
|
||||
// ? "text-green-500"
|
||||
// : ""
|
||||
// }`}
|
||||
// >
|
||||
// {childIssue.priority ?? "None"}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.state && (
|
||||
// <div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
// <span
|
||||
// className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
// style={{ backgroundColor: childIssue.state_detail.color }}
|
||||
// ></span>
|
||||
// {addSpaceIfCamelCase(childIssue.state_detail.name)}
|
||||
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1">State</h5>
|
||||
// <div>{childIssue.state_detail.name}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.start_date && (
|
||||
// <div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
// <CalendarDaysIcon className="h-4 w-4" />
|
||||
// {childIssue.start_date
|
||||
// ? renderShortNumericDateFormat(childIssue.start_date)
|
||||
// : "N/A"}
|
||||
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1">Started at</h5>
|
||||
// <div>
|
||||
// {renderShortNumericDateFormat(childIssue.start_date ?? "")}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.target_date && (
|
||||
// <div
|
||||
// className={`group flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
// childIssue.target_date === null
|
||||
// ? ""
|
||||
// : childIssue.target_date < new Date().toISOString()
|
||||
// ? "text-red-600"
|
||||
// : findHowManyDaysLeft(childIssue.target_date) <= 3 &&
|
||||
// "text-orange-400"
|
||||
// }`}
|
||||
// >
|
||||
// <CalendarDaysIcon className="h-4 w-4" />
|
||||
// {childIssue.target_date
|
||||
// ? renderShortNumericDateFormat(childIssue.target_date)
|
||||
// : "N/A"}
|
||||
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1 text-gray-900">Target date</h5>
|
||||
// <div>
|
||||
// {renderShortNumericDateFormat(childIssue.target_date ?? "")}
|
||||
// </div>
|
||||
// <div>
|
||||
// {childIssue.target_date &&
|
||||
// (childIssue.target_date < new Date().toISOString()
|
||||
// ? `Target date has passed by ${findHowManyDaysLeft(
|
||||
// childIssue.target_date
|
||||
// )} days`
|
||||
// : findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||
// ? `Target date is in ${findHowManyDaysLeft(
|
||||
// childIssue.target_date
|
||||
// )} days`
|
||||
// : "Target date")}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.assignee && (
|
||||
// <div className="group flex items-center gap-1 text-xs">
|
||||
// {childIssue.assignee_details?.length > 0 ? (
|
||||
// childIssue.assignee_details?.map((assignee, index: number) => (
|
||||
// <div
|
||||
// key={index}
|
||||
// className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
// index !== 0 ? "-ml-2.5" : ""
|
||||
// }`}
|
||||
// >
|
||||
// {assignee.avatar && assignee.avatar !== "" ? (
|
||||
// <div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
// <Image
|
||||
// src={assignee.avatar}
|
||||
// height="100%"
|
||||
// width="100%"
|
||||
// className="rounded-full"
|
||||
// alt={assignee.name}
|
||||
// />
|
||||
// </div>
|
||||
// ) : (
|
||||
// <div
|
||||
// className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
// >
|
||||
// {assignee.first_name.charAt(0)}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// ))
|
||||
// ) : (
|
||||
// <div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
// <Image
|
||||
// src={User}
|
||||
// height="100%"
|
||||
// width="100%"
|
||||
// className="rounded-full"
|
||||
// alt="No user"
|
||||
// />
|
||||
// </div>
|
||||
// )}
|
||||
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1">Assigned to</h5>
|
||||
// <div>
|
||||
// {childIssue.assignee_details?.length > 0
|
||||
// ? childIssue.assignee_details
|
||||
// .map((assignee) => assignee.first_name)
|
||||
// .join(", ")
|
||||
// : "No one"}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// ))
|
||||
// ) : (
|
||||
// <div className="w-full h-full flex justify-center items-center">
|
||||
// <Spinner />
|
||||
// </div>
|
||||
// )}
|
||||
// <button
|
||||
// type="button"
|
||||
// className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
|
||||
// onClick={() => openCreateIssueModal(cycle.id)}
|
||||
// >
|
||||
// <PlusIcon className="h-3 w-3 mr-1" />
|
||||
// Create
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
};
|
||||
|
||||
export default SingleCycleBoard;
|
@ -1,714 +0,0 @@
|
||||
// react
|
||||
import React from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// headless ui
|
||||
import { Disclosure, Transition, Menu } from "@headlessui/react";
|
||||
// services
|
||||
import cycleServices from "lib/services/cycles.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// icons
|
||||
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, SelectSprintType } from "types";
|
||||
// fetch keys
|
||||
import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
|
||||
type Props = {
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
properties: Properties;
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
openCreateIssueModal: (
|
||||
sprintId: string,
|
||||
issue?: IIssue,
|
||||
actionType?: "create" | "edit" | "delete"
|
||||
) => void;
|
||||
openIssuesListModal: (cycleId: string) => void;
|
||||
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
|
||||
};
|
||||
|
||||
const CyclesListView: React.FC<Props> = ({
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
properties,
|
||||
removeIssueFromCycle,
|
||||
}) => {
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-5">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => (
|
||||
<Disclosure key={singleGroup} as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
||||
/>
|
||||
</span>
|
||||
{selectedGroup !== null ? (
|
||||
<h2 className="font-medium leading-5 capitalize">
|
||||
{singleGroup === null || singleGroup === "null"
|
||||
? selectedGroup === "priority" && "No priority"
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
)}
|
||||
<p className="text-gray-500 text-sm">
|
||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||
</p>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="divide-y-2">
|
||||
{groupedByIssues[singleGroup] ? (
|
||||
groupedByIssues[singleGroup].length > 0 ? (
|
||||
groupedByIssues[singleGroup].map((issue: IIssue) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find(
|
||||
(p) => p.member.id === assignee
|
||||
)?.member;
|
||||
|
||||
return {
|
||||
avatar: tempPerson?.avatar,
|
||||
first_name: tempPerson?.first_name,
|
||||
email: tempPerson?.email,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
<span className="">{issue.name}</span>
|
||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Name</h5>
|
||||
<div>{issue.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
{properties.priority && (
|
||||
<div
|
||||
className={`group relative flex-shrink-0 flex items-center gap-1 text-xs rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(issue.priority ?? "")} */}
|
||||
{issue.priority ?? "None"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||
<div
|
||||
className={`capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "text-green-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.state && (
|
||||
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue?.state_detail?.color,
|
||||
}}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(issue?.state_detail.name)}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">State</h5>
|
||||
<div>{issue?.state_detail.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.start_date && (
|
||||
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.start_date
|
||||
? renderShortNumericDateFormat(issue.start_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Started at</h5>
|
||||
<div>
|
||||
{renderShortNumericDateFormat(issue.start_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.target_date && (
|
||||
<div
|
||||
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 &&
|
||||
"text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">
|
||||
Target date
|
||||
</h5>
|
||||
<div>
|
||||
{renderShortNumericDateFormat(issue.target_date ?? "")}
|
||||
</div>
|
||||
<div>
|
||||
{issue.target_date &&
|
||||
(issue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: "Target date")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button
|
||||
as="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
type="button"
|
||||
// onClick={() =>
|
||||
// openCreateIssueModal(cycle.id, issue, "edit")
|
||||
// }
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||
<button
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
type="button"
|
||||
// onClick={() =>
|
||||
// removeIssueFromCycle(issue.cycle, issue.id)
|
||||
// }
|
||||
>
|
||||
Remove from cycle
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||
<button
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
type="button"
|
||||
// onClick={() =>
|
||||
// openCreateIssueModal(cycle.id, issue, "delete")
|
||||
// }
|
||||
>
|
||||
Delete permanently
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
|
||||
)
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
<div className="p-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
|
||||
// onClick={() => {
|
||||
// setIsCreateIssuesModalOpen(true);
|
||||
// if (selectedGroup !== null) {
|
||||
// const stateId =
|
||||
// selectedGroup === "state_detail.name"
|
||||
// ? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
// : null;
|
||||
// setPreloadedData({
|
||||
// state: stateId !== null ? stateId : undefined,
|
||||
// [selectedGroup]: singleGroup,
|
||||
// actionType: "createIssue",
|
||||
// });
|
||||
// }
|
||||
// }}
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add issue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <Disclosure as="div" defaultOpen>
|
||||
// {({ open }) => (
|
||||
// <div className="bg-white rounded-lg">
|
||||
// <div className="flex justify-between items-center bg-gray-100 px-4 py-3 rounded-t-lg">
|
||||
// <div className="flex items-center gap-2">
|
||||
// <Disclosure.Button>
|
||||
// <ChevronDownIcon
|
||||
// className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
||||
// />
|
||||
// </Disclosure.Button>
|
||||
// <Link href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
|
||||
// <a className="flex items-center gap-2">
|
||||
// <h2 className="font-medium leading-5">{cycle.name}</h2>
|
||||
// <p className="flex gap-2 text-xs text-gray-500">
|
||||
// <span>
|
||||
// {cycle.status === "started"
|
||||
// ? cycle.start_date
|
||||
// ? `${renderShortNumericDateFormat(cycle.start_date)} - `
|
||||
// : ""
|
||||
// : cycle.status}
|
||||
// </span>
|
||||
// <span>
|
||||
// {cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""}
|
||||
// </span>
|
||||
// </p>
|
||||
// </a>
|
||||
// </Link>
|
||||
// <p className="text-gray-500 text-sm ml-0.5">{cycleIssues?.length}</p>
|
||||
// </div>
|
||||
|
||||
// <Menu as="div" className="relative inline-block">
|
||||
// <Menu.Button
|
||||
// as="button"
|
||||
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none`}
|
||||
// >
|
||||
// <EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
// </Menu.Button>
|
||||
// <Menu.Items className="absolute origin-top-right right-0 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
// <Menu.Item>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// onClick={() => selectSprint({ ...cycle, actionType: "edit" })}
|
||||
// >
|
||||
// Edit
|
||||
// </button>
|
||||
// </Menu.Item>
|
||||
// <Menu.Item>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// onClick={() => selectSprint({ ...cycle, actionType: "delete" })}
|
||||
// >
|
||||
// Delete
|
||||
// </button>
|
||||
// </Menu.Item>
|
||||
// </Menu.Items>
|
||||
// </Menu>
|
||||
// </div>
|
||||
// <Transition
|
||||
// show={open}
|
||||
// enter="transition duration-100 ease-out"
|
||||
// enterFrom="transform opacity-0"
|
||||
// enterTo="transform opacity-100"
|
||||
// leave="transition duration-75 ease-out"
|
||||
// leaveFrom="transform opacity-100"
|
||||
// leaveTo="transform opacity-0"
|
||||
// >
|
||||
// <Disclosure.Panel>
|
||||
// <StrictModeDroppable droppableId={cycle.id}>
|
||||
// {(provided) => (
|
||||
// <div
|
||||
// ref={provided.innerRef}
|
||||
// {...provided.droppableProps}
|
||||
// className="divide-y-2"
|
||||
// >
|
||||
// {cycleIssues ? (
|
||||
// cycleIssues.length > 0 ? (
|
||||
// cycleIssues.map((issue, index) => (
|
||||
// <Draggable
|
||||
// key={issue.id}
|
||||
// draggableId={`${issue.id},${issue.issue}`} // bridge id, issue id
|
||||
// index={index}
|
||||
// >
|
||||
// {(provided, snapshot) => (
|
||||
// <div
|
||||
// className={`px-2 py-3 text-sm rounded flex justify-between items-center gap-2 ${
|
||||
// snapshot.isDragging
|
||||
// ? "bg-gray-100 shadow-lg border border-theme"
|
||||
// : ""
|
||||
// }`}
|
||||
// ref={provided.innerRef}
|
||||
// {...provided.draggableProps}
|
||||
// >
|
||||
// <div className="flex items-center gap-2">
|
||||
// <button
|
||||
// type="button"
|
||||
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 rotate-90 outline-none`}
|
||||
// {...provided.dragHandleProps}
|
||||
// >
|
||||
// <EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
||||
// <EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
|
||||
// </button>
|
||||
// <span
|
||||
// className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||
// style={{
|
||||
// backgroundColor: issue?.state_detail?.color,
|
||||
// }}
|
||||
// />
|
||||
// <Link
|
||||
// href={`/projects/${projectId}/issues/${issue.id}`}
|
||||
// >
|
||||
// <a className="flex items-center gap-2">
|
||||
// {properties.key && (
|
||||
// <span className="flex-shrink-0 text-xs text-gray-500">
|
||||
// {activeProject?.identifier}-
|
||||
// {issue.sequence_id}
|
||||
// </span>
|
||||
// )}
|
||||
// <span>{issue.name}</span>
|
||||
// </a>
|
||||
// </Link>
|
||||
// </div>
|
||||
// <div className="flex items-center gap-2">
|
||||
// {properties.priority && (
|
||||
// <div
|
||||
// className={`group relative flex-shrink-0 flex items-center gap-1 text-xs rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
// issue.priority === "urgent"
|
||||
// ? "bg-red-100 text-red-600"
|
||||
// : issue.priority === "high"
|
||||
// ? "bg-orange-100 text-orange-500"
|
||||
// : issue.priority === "medium"
|
||||
// ? "bg-yellow-100 text-yellow-500"
|
||||
// : issue.priority === "low"
|
||||
// ? "bg-green-100 text-green-500"
|
||||
// : "bg-gray-100"
|
||||
// }`}
|
||||
// >
|
||||
// {/* {getPriorityIcon(issue.priority ?? "")} */}
|
||||
// {issue.priority ?? "None"}
|
||||
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1 text-gray-900">
|
||||
// Priority
|
||||
// </h5>
|
||||
// <div
|
||||
// className={`capitalize ${
|
||||
// issue.priority === "urgent"
|
||||
// ? "text-red-600"
|
||||
// : issue.priority === "high"
|
||||
// ? "text-orange-500"
|
||||
// : issue.priority === "medium"
|
||||
// ? "text-yellow-500"
|
||||
// : issue.priority === "low"
|
||||
// ? "text-green-500"
|
||||
// : ""
|
||||
// }`}
|
||||
// >
|
||||
// {issue.priority ?? "None"}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.state && (
|
||||
// <div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
// <span
|
||||
// className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
// style={{
|
||||
// backgroundColor:
|
||||
// issue?.state_detail?.color,
|
||||
// }}
|
||||
// ></span>
|
||||
// {addSpaceIfCamelCase(
|
||||
// issue?.state_detail.name
|
||||
// )}
|
||||
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1">State</h5>
|
||||
// <div>{issue?.state_detail.name}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.start_date && (
|
||||
// <div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
// <CalendarDaysIcon className="h-4 w-4" />
|
||||
// {issue.start_date
|
||||
// ? renderShortNumericDateFormat(
|
||||
// issue.start_date
|
||||
// )
|
||||
// : "N/A"}
|
||||
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1">Started at</h5>
|
||||
// <div>
|
||||
// {renderShortNumericDateFormat(
|
||||
// issue.start_date ?? ""
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// {properties.target_date && (
|
||||
// <div
|
||||
// className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
// issue.target_date === null
|
||||
// ? ""
|
||||
// : issue.target_date <
|
||||
// new Date().toISOString()
|
||||
// ? "text-red-600"
|
||||
// : findHowManyDaysLeft(
|
||||
// issue.target_date
|
||||
// ) <= 3 && "text-orange-400"
|
||||
// }`}
|
||||
// >
|
||||
// <CalendarDaysIcon className="h-4 w-4" />
|
||||
// {issue.target_date
|
||||
// ? renderShortNumericDateFormat(
|
||||
// issue.target_date
|
||||
// )
|
||||
// : "N/A"}
|
||||
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
// <h5 className="font-medium mb-1 text-gray-900">
|
||||
// Target date
|
||||
// </h5>
|
||||
// <div>
|
||||
// {renderShortNumericDateFormat(
|
||||
// issue.target_date ?? ""
|
||||
// )}
|
||||
// </div>
|
||||
// <div>
|
||||
// {issue.target_date &&
|
||||
// (issue.target_date <
|
||||
// new Date().toISOString()
|
||||
// ? `Target date has passed by ${findHowManyDaysLeft(
|
||||
// issue.target_date
|
||||
// )} days`
|
||||
// : findHowManyDaysLeft(
|
||||
// issue.target_date
|
||||
// ) <= 3
|
||||
// ? `Target date is in ${findHowManyDaysLeft(
|
||||
// issue.target_date
|
||||
// )} days`
|
||||
// : "Target date")}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// <Menu as="div" className="relative">
|
||||
// <Menu.Button
|
||||
// as="button"
|
||||
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||
// >
|
||||
// <EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
// </Menu.Button>
|
||||
// <Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
// <Menu.Item>
|
||||
// <button
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// type="button"
|
||||
// onClick={() =>
|
||||
// openCreateIssueModal(
|
||||
// cycle.id,
|
||||
// issue,
|
||||
// "edit"
|
||||
// )
|
||||
// }
|
||||
// >
|
||||
// Edit
|
||||
// </button>
|
||||
// </Menu.Item>
|
||||
// <Menu.Item>
|
||||
// <div className="hover:bg-gray-100 border-b last:border-0">
|
||||
// <button
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// type="button"
|
||||
// onClick={() =>
|
||||
// removeIssueFromCycle(issue.cycle, issue.id)
|
||||
// }
|
||||
// >
|
||||
// Remove from cycle
|
||||
// </button>
|
||||
// </div>
|
||||
// </Menu.Item>
|
||||
// <Menu.Item>
|
||||
// <div className="hover:bg-gray-100 border-b last:border-0">
|
||||
// <button
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// type="button"
|
||||
// onClick={() =>
|
||||
// openCreateIssueModal(
|
||||
// cycle.id,
|
||||
// issue,
|
||||
// "delete"
|
||||
// )
|
||||
// }
|
||||
// >
|
||||
// Delete permanently
|
||||
// </button>
|
||||
// </div>
|
||||
// </Menu.Item>
|
||||
// </Menu.Items>
|
||||
// </Menu>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// </Draggable>
|
||||
// ))
|
||||
// ) : (
|
||||
// <p className="text-sm px-4 py-3 text-gray-500">
|
||||
// This cycle has no issue.
|
||||
// </p>
|
||||
// )
|
||||
// ) : (
|
||||
// <div className="w-full h-full flex items-center justify-center">
|
||||
// <Spinner />
|
||||
// </div>
|
||||
// )}
|
||||
// {provided.placeholder}
|
||||
// </div>
|
||||
// )}
|
||||
// </StrictModeDroppable>
|
||||
// </Disclosure.Panel>
|
||||
// </Transition>
|
||||
// <div className="p-3">
|
||||
// <Menu as="div" className="relative inline-block">
|
||||
// <Menu.Button className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
|
||||
// <PlusIcon className="h-3 w-3" />
|
||||
// Add issue
|
||||
// </Menu.Button>
|
||||
|
||||
// <Transition
|
||||
// as={React.Fragment}
|
||||
// enter="transition ease-out duration-100"
|
||||
// enterFrom="transform opacity-0 scale-95"
|
||||
// enterTo="transform opacity-100 scale-100"
|
||||
// leave="transition ease-in duration-75"
|
||||
// leaveFrom="transform opacity-100 scale-100"
|
||||
// leaveTo="transform opacity-0 scale-95"
|
||||
// >
|
||||
// <Menu.Items className="absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
// <div className="p-1">
|
||||
// <Menu.Item as="div">
|
||||
// {(active) => (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// onClick={() => openCreateIssueModal(cycle.id)}
|
||||
// >
|
||||
// Create new
|
||||
// </button>
|
||||
// )}
|
||||
// </Menu.Item>
|
||||
// <Menu.Item as="div">
|
||||
// {(active) => (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||
// onClick={() => openIssuesListModal(cycle.id)}
|
||||
// >
|
||||
// Add an existing issue
|
||||
// </button>
|
||||
// )}
|
||||
// </Menu.Item>
|
||||
// </div>
|
||||
// </Menu.Items>
|
||||
// </Transition>
|
||||
// </Menu>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// </Disclosure>
|
||||
// </>
|
||||
// );
|
||||
};
|
||||
|
||||
export default CyclesListView;
|
@ -1,5 +1,5 @@
|
||||
// components
|
||||
import SingleBoard from "components/project/cycles/BoardView/single-board";
|
||||
import SingleBoard from "components/project/cycles/board-view/single-board";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// types
|
||||
@ -13,11 +13,7 @@ type Props = {
|
||||
properties: Properties;
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
members: IProjectMember[] | undefined;
|
||||
openCreateIssueModal: (
|
||||
sprintId: string,
|
||||
issue?: IIssue,
|
||||
actionType?: "create" | "edit" | "delete"
|
||||
) => void;
|
||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||
openIssuesListModal: (cycleId: string) => void;
|
||||
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
|
||||
};
|
377
apps/app/components/project/cycles/board-view/single-board.tsx
Normal file
377
apps/app/components/project/cycles/board-view/single-board.tsx
Normal file
@ -0,0 +1,377 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import cycleServices from "lib/services/cycles.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// icons
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
CalendarDaysIcon,
|
||||
PlusIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import {
|
||||
CycleIssueResponse,
|
||||
ICycle,
|
||||
IIssue,
|
||||
IWorkspaceMember,
|
||||
NestedKeyOf,
|
||||
Properties,
|
||||
} from "types";
|
||||
// constants
|
||||
import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
|
||||
type Props = {
|
||||
properties: Properties;
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
groupTitle: string;
|
||||
createdBy: string | null;
|
||||
bgColor?: string;
|
||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||
openIssuesListModal: (cycleId: string) => void;
|
||||
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
|
||||
};
|
||||
|
||||
const SingleCycleBoard: React.FC<Props> = ({
|
||||
properties,
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
groupTitle,
|
||||
createdBy,
|
||||
bgColor,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
removeIssueFromCycle,
|
||||
}) => {
|
||||
// Collapse/Expand
|
||||
const [show, setState] = useState(true);
|
||||
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
if (selectedGroup === "priority")
|
||||
groupTitle === "high"
|
||||
? (bgColor = "#dc2626")
|
||||
: groupTitle === "medium"
|
||||
? (bgColor = "#f97316")
|
||||
: groupTitle === "low"
|
||||
? (bgColor = "#22c55e")
|
||||
: (bgColor = "#ff0000");
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`rounded flex-shrink-0 h-full ${!show ? "" : "w-80 bg-gray-50 border"}`}>
|
||||
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
|
||||
<div
|
||||
className={`flex justify-between p-3 pb-0 ${
|
||||
!show ? "flex-col bg-gray-50 rounded-md border" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-full flex justify-between items-center ${
|
||||
!show ? "flex-col gap-2" : "gap-1"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
|
||||
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
|
||||
}`}
|
||||
style={{
|
||||
border: `2px solid ${bgColor}`,
|
||||
backgroundColor: `${bgColor}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
|
||||
style={{
|
||||
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
|
||||
}}
|
||||
/>
|
||||
<h2
|
||||
className={`text-[0.9rem] font-medium capitalize`}
|
||||
style={{
|
||||
writingMode: !show ? "vertical-rl" : "horizontal-tb",
|
||||
}}
|
||||
>
|
||||
{groupTitle === null || groupTitle === "null"
|
||||
? "None"
|
||||
: createdBy
|
||||
? createdBy
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
</h2>
|
||||
<span className="text-gray-500 text-sm ml-0.5">
|
||||
{groupedByIssues[groupTitle].length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block">
|
||||
<Menu.Button className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none">
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
<div className="p-1">
|
||||
<Menu.Item as="div">
|
||||
{(active) => (
|
||||
<button
|
||||
type="button"
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
onClick={() => openCreateIssueModal()}
|
||||
>
|
||||
Create new
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item as="div">
|
||||
{(active) => (
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||
// onClick={() => openIssuesListModal()}
|
||||
>
|
||||
Add an existing issue
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
|
||||
!show ? "hidden" : "block"
|
||||
}`}
|
||||
>
|
||||
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
|
||||
const assignees = [
|
||||
...(childIssue?.assignees_list ?? []),
|
||||
...(childIssue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
|
||||
|
||||
return {
|
||||
avatar: tempPerson?.avatar,
|
||||
first_name: tempPerson?.first_name,
|
||||
email: tempPerson?.email,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={childIssue.id} className={`border rounded bg-white shadow-sm`}>
|
||||
<div className="relative p-2 select-none">
|
||||
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
|
||||
<a>
|
||||
{properties.key && (
|
||||
<div className="text-xs font-medium text-gray-500 mb-2">
|
||||
{activeProject?.identifier}-{childIssue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
<h5
|
||||
className="group-hover:text-theme text-sm break-all mb-3"
|
||||
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
||||
>
|
||||
{childIssue.name}
|
||||
</h5>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
{properties.priority && (
|
||||
<div
|
||||
className={`group flex-shrink-0 flex items-center gap-1 rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
childIssue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: childIssue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: childIssue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: childIssue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(childIssue.priority ?? "")} */}
|
||||
{childIssue.priority ?? "None"}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||
<div
|
||||
className={`capitalize ${
|
||||
childIssue.priority === "urgent"
|
||||
? "text-red-600"
|
||||
: childIssue.priority === "high"
|
||||
? "text-orange-500"
|
||||
: childIssue.priority === "medium"
|
||||
? "text-yellow-500"
|
||||
: childIssue.priority === "low"
|
||||
? "text-green-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{childIssue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.state && (
|
||||
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{ backgroundColor: childIssue.state_detail.color }}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(childIssue.state_detail.name)}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">State</h5>
|
||||
<div>{childIssue.state_detail.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.start_date && (
|
||||
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.start_date
|
||||
? renderShortNumericDateFormat(childIssue.start_date)
|
||||
: "N/A"}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Started at</h5>
|
||||
<div>{renderShortNumericDateFormat(childIssue.start_date ?? "")}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.target_date && (
|
||||
<div
|
||||
className={`group flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
childIssue.target_date === null
|
||||
? ""
|
||||
: childIssue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.target_date
|
||||
? renderShortNumericDateFormat(childIssue.target_date)
|
||||
: "N/A"}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Target date</h5>
|
||||
<div>{renderShortNumericDateFormat(childIssue.target_date ?? "")}</div>
|
||||
<div>
|
||||
{childIssue.target_date &&
|
||||
(childIssue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: "Target date")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="group flex items-center gap-1 text-xs">
|
||||
{childIssue.assignee_details?.length > 0 ? (
|
||||
childIssue.assignee_details?.map((assignee, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
{assignee.avatar && assignee.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={assignee.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={assignee.name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{assignee.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Assigned to</h5>
|
||||
<div>
|
||||
{childIssue.assignee_details?.length > 0
|
||||
? childIssue.assignee_details
|
||||
.map((assignee) => assignee.first_name)
|
||||
.join(", ")
|
||||
: "No one"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3 mr-1" />
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleCycleBoard;
|
352
apps/app/components/project/cycles/list-view/index.tsx
Normal file
352
apps/app/components/project/cycles/list-view/index.tsx
Normal file
@ -0,0 +1,352 @@
|
||||
// react
|
||||
import React from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// headless ui
|
||||
import { Disclosure, Transition, Menu } from "@headlessui/react";
|
||||
// services
|
||||
import cycleServices from "lib/services/cycles.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// icons
|
||||
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, SelectSprintType } from "types";
|
||||
// fetch keys
|
||||
import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
|
||||
type Props = {
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
properties: Properties;
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||
openIssuesListModal: (cycleId: string) => void;
|
||||
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
|
||||
};
|
||||
|
||||
const CyclesListView: React.FC<Props> = ({
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
properties,
|
||||
removeIssueFromCycle,
|
||||
}) => {
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-5">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => (
|
||||
<Disclosure key={singleGroup} as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
||||
/>
|
||||
</span>
|
||||
{selectedGroup !== null ? (
|
||||
<h2 className="font-medium leading-5 capitalize">
|
||||
{singleGroup === null || singleGroup === "null"
|
||||
? selectedGroup === "priority" && "No priority"
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
)}
|
||||
<p className="text-gray-500 text-sm">
|
||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||
</p>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="divide-y-2">
|
||||
{groupedByIssues[singleGroup] ? (
|
||||
groupedByIssues[singleGroup].length > 0 ? (
|
||||
groupedByIssues[singleGroup].map((issue: IIssue) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find(
|
||||
(p) => p.member.id === assignee
|
||||
)?.member;
|
||||
|
||||
return {
|
||||
avatar: tempPerson?.avatar,
|
||||
first_name: tempPerson?.first_name,
|
||||
email: tempPerson?.email,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
<span className="">{issue.name}</span>
|
||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Name</h5>
|
||||
<div>{issue.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
{properties.priority && (
|
||||
<div
|
||||
className={`group relative flex-shrink-0 flex items-center gap-1 text-xs rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(issue.priority ?? "")} */}
|
||||
{issue.priority ?? "None"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||
<div
|
||||
className={`capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "text-green-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.state && (
|
||||
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue?.state_detail?.color,
|
||||
}}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(issue?.state_detail.name)}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">State</h5>
|
||||
<div>{issue?.state_detail.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.start_date && (
|
||||
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.start_date
|
||||
? renderShortNumericDateFormat(issue.start_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Started at</h5>
|
||||
<div>
|
||||
{renderShortNumericDateFormat(issue.start_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.target_date && (
|
||||
<div
|
||||
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 &&
|
||||
"text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">
|
||||
Target date
|
||||
</h5>
|
||||
<div>
|
||||
{renderShortNumericDateFormat(issue.target_date ?? "")}
|
||||
</div>
|
||||
<div>
|
||||
{issue.target_date &&
|
||||
(issue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: "Target date")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button
|
||||
as="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
type="button"
|
||||
onClick={() => openCreateIssueModal(issue, "edit")}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||
<button
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
type="button"
|
||||
// onClick={() =>
|
||||
// removeIssueFromCycle(issue.cycle, issue.id)
|
||||
// }
|
||||
>
|
||||
Remove from cycle
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||
<button
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
type="button"
|
||||
// onClick={() =>
|
||||
// openCreateIssueModal(cycle.id, issue, "delete")
|
||||
// }
|
||||
>
|
||||
Delete permanently
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
|
||||
)
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
<div className="p-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
|
||||
// onClick={() => {
|
||||
// setIsCreateIssuesModalOpen(true);
|
||||
// if (selectedGroup !== null) {
|
||||
// const stateId =
|
||||
// selectedGroup === "state_detail.name"
|
||||
// ? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
// : null;
|
||||
// setPreloadedData({
|
||||
// state: stateId !== null ? stateId : undefined,
|
||||
// [selectedGroup]: singleGroup,
|
||||
// actionType: "createIssue",
|
||||
// });
|
||||
// }
|
||||
// }}
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add issue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
))}
|
||||
</div>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// onClick={() => selectSprint({ ...cycle, actionType: "edit" })}
|
||||
// >
|
||||
// Edit
|
||||
// </button>
|
||||
// </Menu.Item>
|
||||
// <Menu.Item>
|
||||
// <button
|
||||
// type="button"
|
||||
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
// onClick={() => selectSprint({ ...cycle, actionType: "delete" })}
|
||||
// >
|
||||
// Delete
|
||||
// </button>
|
||||
// </Menu.Item
|
||||
);
|
||||
};
|
||||
|
||||
export default CyclesListView;
|
23
apps/app/components/project/cycles/stats-view/index.tsx
Normal file
23
apps/app/components/project/cycles/stats-view/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
// next
|
||||
import Link from "next/link";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
import SingleStat from "./single-stat";
|
||||
|
||||
type Props = {
|
||||
cycles: ICycle[];
|
||||
};
|
||||
|
||||
const CycleStatsView: React.FC<Props> = ({ cycles }) => {
|
||||
return (
|
||||
<>
|
||||
{cycles.map((cycle) => (
|
||||
<SingleStat key={cycle.id} cycle={cycle} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CycleStatsView;
|
102
apps/app/components/project/cycles/stats-view/single-stat.tsx
Normal file
102
apps/app/components/project/cycles/stats-view/single-stat.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
// next
|
||||
import Link from "next/link";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import cyclesService from "lib/services/cycles.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// types
|
||||
import { CycleIssueResponse, ICycle } from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_ISSUES } from "constants/fetch-keys";
|
||||
import { groupBy, renderShortNumericDateFormat } from "constants/common";
|
||||
import {
|
||||
CheckIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = { cycle: ICycle };
|
||||
|
||||
const stateGroupIcons: {
|
||||
[key: string]: JSX.Element;
|
||||
} = {
|
||||
backlog: <ExclamationTriangleIcon className="h-4 w-4" />,
|
||||
unstarted: <CheckIcon className="h-4 w-4" />,
|
||||
started: <CheckIcon className="h-4 w-4" />,
|
||||
cancelled: <ExclamationCircleIcon className="h-4 w-4" />,
|
||||
completed: <CheckIcon className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
const SingleStat: React.FC<Props> = ({ cycle }) => {
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
||||
activeWorkspace && activeProject && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null,
|
||||
activeWorkspace && activeProject && cycle.id
|
||||
? () =>
|
||||
cyclesService.getCycleIssues(activeWorkspace?.slug, activeProject?.id, cycle.id as string)
|
||||
: null
|
||||
);
|
||||
const groupedIssues = {
|
||||
backlog: [],
|
||||
unstarted: [],
|
||||
started: [],
|
||||
cancelled: [],
|
||||
completed: [],
|
||||
...groupBy(cycleIssues ?? [], "issue_details.state_detail.group"),
|
||||
};
|
||||
|
||||
// status calculator
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const today = new Date();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white p-3">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
|
||||
<a className="block rounded-md">{cycle.name}</a>
|
||||
</Link>
|
||||
<div className="text-xs text-gray-500">
|
||||
<span>{renderShortNumericDateFormat(startDate)}</span>
|
||||
{" - "}
|
||||
<span>{renderShortNumericDateFormat(endDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs bg-gray-100 px-2 py-1 rounded">
|
||||
{today.getDate() < startDate.getDate()
|
||||
? "Not started"
|
||||
: today.getDate() > endDate.getDate()
|
||||
? "Over"
|
||||
: "Active"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm mt-6 mb-4 space-y-2">
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{Object.keys(groupedIssues).map((group) => {
|
||||
return (
|
||||
<div key={group} className="bg-gray-100 rounded-md p-3 flex items-center gap-3">
|
||||
<div>
|
||||
<div className="h-8 w-8 rounded-full grid place-items-center bg-green-500 text-white">
|
||||
{stateGroupIcons[group]}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="capitalize">{group}</h5>
|
||||
<span>{groupedIssues[group].length}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleStat;
|
@ -19,6 +19,7 @@ type Props = {
|
||||
issues: IIssue[];
|
||||
title?: string;
|
||||
multiple?: boolean;
|
||||
customDisplay?: JSX.Element;
|
||||
};
|
||||
|
||||
const IssuesListModal: React.FC<Props> = ({
|
||||
@ -29,6 +30,7 @@ const IssuesListModal: React.FC<Props> = ({
|
||||
issues,
|
||||
title = "Issues",
|
||||
multiple = false,
|
||||
customDisplay,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
@ -90,9 +92,10 @@ const IssuesListModal: React.FC<Props> = ({
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
displayValue={() => ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3">{customDisplay}</div>
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||
|
@ -15,12 +15,7 @@ import useToast from "lib/hooks/useToast";
|
||||
// components
|
||||
import IssuesListModal from "components/project/issues/IssuesListModal";
|
||||
// fetching keys
|
||||
import {
|
||||
PROJECT_ISSUES_LIST,
|
||||
STATE_LIST,
|
||||
WORKSPACE_MEMBERS,
|
||||
PROJECT_ISSUE_LABELS,
|
||||
} from "constants/fetch-keys";
|
||||
import { STATE_LIST, WORKSPACE_MEMBERS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
// commons
|
||||
import { classNames, copyTextToClipboard } from "constants/common";
|
||||
import { PRIORITIES } from "constants/";
|
||||
@ -48,6 +43,11 @@ import type { Control } from "react-hook-form";
|
||||
import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import { positionEditorElement } from "components/lexical/helpers/editor";
|
||||
import SelectState from "./select-state";
|
||||
import SelectPriority from "./select-priority";
|
||||
import SelectParent from "./select-parent";
|
||||
import SelectCycle from "./select-cycle";
|
||||
import SelectAssignee from "./select-assignee";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
@ -71,25 +71,12 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
}) => {
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||
|
||||
const { activeWorkspace, activeProject, cycles, issues } = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: states } = useSWR<IState[]>(
|
||||
activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null,
|
||||
activeWorkspace && activeProject
|
||||
? () => stateServices.getStates(activeWorkspace.slug, activeProject.id)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: people } = useSWR(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
||||
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
||||
activeProject && activeWorkspace
|
||||
@ -108,7 +95,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onSubmit = (formData: any) => {
|
||||
const handleNewLabel = (formData: any) => {
|
||||
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
||||
issuesServices
|
||||
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
|
||||
@ -133,58 +120,6 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
}>
|
||||
> = [
|
||||
[
|
||||
{
|
||||
label: "Status",
|
||||
name: "state",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: Squares2X2Icon,
|
||||
options: states?.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.id,
|
||||
color: state.color,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
{
|
||||
label: "Assignees",
|
||||
name: "assignees_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserGroupIcon,
|
||||
options: people?.map((person) => ({
|
||||
label: person.member.first_name,
|
||||
value: person.member.id,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
{
|
||||
label: "Priority",
|
||||
name: "priority",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: ChartBarIcon,
|
||||
options: PRIORITIES.map((property) => ({
|
||||
label: property,
|
||||
value: property,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
label: "Parent",
|
||||
name: "parent",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: UserIcon,
|
||||
issuesList:
|
||||
issues?.results.filter(
|
||||
(i) =>
|
||||
i.id !== issueDetail?.id &&
|
||||
i.id !== issueDetail?.parent &&
|
||||
i.parent !== issueDetail?.id
|
||||
) ?? [],
|
||||
modal: true,
|
||||
isOpen: isParentModalOpen,
|
||||
setIsOpen: setIsParentModalOpen,
|
||||
},
|
||||
// {
|
||||
// label: "Blocker",
|
||||
// name: "blockers_list",
|
||||
@ -205,26 +140,6 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
// isOpen: isBlockedModalOpen,
|
||||
// setIsOpen: setIsBlockedModalOpen,
|
||||
// },
|
||||
{
|
||||
label: "Target Date",
|
||||
name: "target_date",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: CalendarDaysIcon,
|
||||
modal: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
label: "Cycle",
|
||||
name: "cycle",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: ArrowPathIcon,
|
||||
options: cycles?.map((cycle) => ({
|
||||
label: cycle.name,
|
||||
value: cycle.id,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
@ -297,7 +212,69 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y-2 divide-gray-100">
|
||||
{sidebarSections.map((section, index) => (
|
||||
<div className="py-1">
|
||||
<SelectState control={control} submitChanges={submitChanges} />
|
||||
<SelectAssignee control={control} submitChanges={submitChanges} />
|
||||
<SelectPriority control={control} submitChanges={submitChanges} />
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<SelectParent
|
||||
control={control}
|
||||
submitChanges={submitChanges}
|
||||
issuesList={
|
||||
issues?.results.filter(
|
||||
(i) =>
|
||||
i.id !== issueDetail?.id &&
|
||||
i.id !== issueDetail?.parent &&
|
||||
i.parent !== issueDetail?.id
|
||||
) ?? []
|
||||
}
|
||||
customDisplay={
|
||||
issueDetail?.parent_detail ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 bg-gray-100 px-3 py-2 text-xs rounded"
|
||||
onClick={() => submitChanges({ parent: null })}
|
||||
>
|
||||
{issueDetail.parent_detail?.name}
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="inline-block bg-gray-100 px-3 py-2 text-xs rounded">
|
||||
No parent selected
|
||||
</div>
|
||||
)
|
||||
}
|
||||
watchIssue={watchIssue}
|
||||
/>
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<CalendarDaysIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<input
|
||||
type="date"
|
||||
value={value ?? ""}
|
||||
onChange={(e: any) => {
|
||||
submitChanges({ target_date: e.target.value });
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<SelectCycle control={control} handleCycleChange={handleCycleChange} />
|
||||
</div>
|
||||
{/* {sidebarSections.map((section, index) => (
|
||||
<div key={index} className="py-1">
|
||||
{section.map((item) => (
|
||||
<div key={item.label} className="flex items-center py-2 flex-wrap">
|
||||
@ -306,154 +283,63 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
<p>{item.label}</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
{item.name === "target_date" ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<input
|
||||
type="date"
|
||||
value={value ?? ""}
|
||||
onChange={(e: any) => {
|
||||
submitChanges({ target_date: e.target.value });
|
||||
onChange(e.target.value);
|
||||
<Controller
|
||||
control={control}
|
||||
name={item.name as keyof IIssue}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<IssuesListModal
|
||||
isOpen={Boolean(item?.isOpen)}
|
||||
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
|
||||
onChange={(val) => {
|
||||
console.log(val);
|
||||
submitChanges({ [item.name]: val });
|
||||
onChange(val);
|
||||
}}
|
||||
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
issues={item?.issuesList ?? []}
|
||||
title={`Select ${item.label}`}
|
||||
multiple={item.canSelectMultipleOptions}
|
||||
value={value}
|
||||
customDisplay={
|
||||
issueDetail?.parent_detail ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 bg-gray-100 px-3 py-2 text-xs rounded"
|
||||
onClick={() => submitChanges({ parent: null })}
|
||||
>
|
||||
{issueDetail.parent_detail?.name}
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="inline-block bg-gray-100 px-3 py-2 text-xs rounded">
|
||||
No parent selected
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : item.modal ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={item.name as keyof IIssue}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<IssuesListModal
|
||||
isOpen={Boolean(item?.isOpen)}
|
||||
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
|
||||
onChange={(val) => {
|
||||
console.log(val);
|
||||
// submitChanges({ [item.name]: val });
|
||||
onChange(val);
|
||||
}}
|
||||
issues={item?.issuesList ?? []}
|
||||
title={`Select ${item.label}`}
|
||||
multiple={item.canSelectMultipleOptions}
|
||||
value={value}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
onClick={() => item.setIsOpen && item.setIsOpen(true)}
|
||||
>
|
||||
{watchIssue(`${item.name as keyof IIssue}`) &&
|
||||
watchIssue(`${item.name as keyof IIssue}`) !== ""
|
||||
? `${activeProject?.identifier}-
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
onClick={() => item.setIsOpen && item.setIsOpen(true)}
|
||||
>
|
||||
{watchIssue(`${item.name as keyof IIssue}`) &&
|
||||
watchIssue(`${item.name as keyof IIssue}`) !== ""
|
||||
? `${activeProject?.identifier}-
|
||||
${
|
||||
issues?.results.find(
|
||||
(i) => i.id === watchIssue(`${item.name as keyof IIssue}`)
|
||||
)?.sequence_id
|
||||
}`
|
||||
: `Select ${item.label}`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
name={item.name as keyof IIssue}
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
multiple={item.canSelectMultipleOptions}
|
||||
onChange={(value: any) => {
|
||||
if (item.name === "cycle") handleCycleChange(value);
|
||||
else submitChanges({ [item.name]: value });
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block text-left",
|
||||
item.label === "Priority" ? "capitalize" : ""
|
||||
)}
|
||||
>
|
||||
{value
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
.map(
|
||||
(i: any) =>
|
||||
item.options?.find((option) => option.value === i)
|
||||
?.label
|
||||
)
|
||||
.join(", ") || item.label
|
||||
: item.options?.find((option) => option.value === value)
|
||||
?.label
|
||||
: "None"}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{item.options ? (
|
||||
item.options.length > 0 ? (
|
||||
item.options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected
|
||||
? "text-white bg-theme"
|
||||
: "text-gray-900"
|
||||
} ${
|
||||
item.label === "Priority" && "capitalize"
|
||||
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
{option.color && (
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: option.color }}
|
||||
></span>
|
||||
)}
|
||||
{option.label}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No {item.label}s found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
: `Select ${item.label}`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
))} */}
|
||||
</div>
|
||||
<div className="pt-3 space-y-3">
|
||||
<div className="flex justify-between items-start">
|
||||
@ -466,18 +352,20 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
{issueDetail?.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="flex items-center gap-2 border rounded-2xl text-xs px-1 py-0.5 hover:bg-gray-100 cursor-pointer"
|
||||
// onClick={() =>
|
||||
// submitChanges({
|
||||
// labels_list: issueDetail?.labels_list.filter((l) => l !== label.id),
|
||||
// })
|
||||
// }
|
||||
className="group flex items-center gap-1 border rounded-2xl text-xs px-1 py-0.5 hover:bg-red-50 hover:border-red-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
const updatedLabels = issueDetail?.labels.filter((l) => l !== label.id);
|
||||
submitChanges({
|
||||
labels_list: updatedLabels,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: label.colour ?? "green" }}
|
||||
></span>
|
||||
{label.name}
|
||||
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
||||
</span>
|
||||
))}
|
||||
<Controller
|
||||
@ -488,22 +376,15 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
as="div"
|
||||
value={value}
|
||||
multiple
|
||||
onChange={(value: any) => submitChanges({ labels_list: value })}
|
||||
onChange={(val) => submitChanges({ labels_list: val })}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex items-center gap-2 border rounded-2xl text-xs px-1 py-0.5 hover:bg-gray-100 cursor-pointer">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden sm:block text-left"
|
||||
)}
|
||||
>
|
||||
Select Label
|
||||
</span>
|
||||
<Listbox.Button className="flex items-center gap-2 border rounded-2xl text-xs px-2 py-0.5 hover:bg-gray-100 cursor-pointer">
|
||||
Select Label
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
@ -551,28 +432,26 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 border rounded-2xl text-xs px-2 py-0.5 hover:bg-gray-100 cursor-pointer"
|
||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||
>
|
||||
{createLabelForm ? (
|
||||
<>
|
||||
<XMarkIcon className="h-3 w-3" /> Cancel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlusIcon className="h-3 w-3" /> New
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
|
||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||
>
|
||||
{createLabelForm ? (
|
||||
<>
|
||||
<XMarkIcon className="h-3 w-3" /> Cancel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlusIcon className="h-3 w-3" /> Create new
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{createLabelForm && (
|
||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
|
||||
<div>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
@ -0,0 +1,181 @@
|
||||
// react
|
||||
import React from "react";
|
||||
// next
|
||||
import Image from "next/image";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// react-hook-form
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// services
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// icons
|
||||
import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { classNames } from "constants/common";
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
};
|
||||
|
||||
const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
const { activeWorkspace } = useUser();
|
||||
|
||||
const { data: people } = useSWR(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<ArrowPathIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignees_list"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
multiple={true}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ assignees_list: value });
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="w-full flex justify-end items-center gap-1 text-xs cursor-pointer">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block text-left"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
{value && Array.isArray(value) ? (
|
||||
<>
|
||||
{value.length > 0 ? (
|
||||
value.map((assignee, index: number) => {
|
||||
const person = people?.find(
|
||||
(p) => p.member.id === assignee
|
||||
)?.member;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
{person && person.avatar && person.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={person.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={person.first_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{person?.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{people ? (
|
||||
people.length > 0 ? (
|
||||
people.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.member.id}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||
}
|
||||
value={option.member.id}
|
||||
>
|
||||
{option.member.avatar && option.member.avatar !== "" ? (
|
||||
<div className="relative h-4 w-4">
|
||||
<Image
|
||||
src={option.member.avatar}
|
||||
alt="avatar"
|
||||
className="rounded-full"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
|
||||
{option.member.first_name && option.member.first_name !== ""
|
||||
? option.member.first_name.charAt(0)
|
||||
: option.member.email.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
{option.member.first_name}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No assignees found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectAssignee;
|
@ -0,0 +1,98 @@
|
||||
// react-hook-form
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { classNames } from "constants/common";
|
||||
import { Spinner } from "ui";
|
||||
import React from "react";
|
||||
import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
handleCycleChange: (cycleId: string) => void;
|
||||
};
|
||||
|
||||
const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
|
||||
const { cycles } = useUser();
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<ArrowPathIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>Cycle</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="cycle"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
handleCycleChange(value);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block text-left"
|
||||
)}
|
||||
>
|
||||
{value ? cycles?.find((c) => c.id === value)?.name : "None"}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
cycles.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.id}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||
}
|
||||
value={option.id}
|
||||
>
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No cycles found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectCycle;
|
@ -0,0 +1,74 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
// react-hook-form
|
||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// components
|
||||
import IssuesListModal from "components/project/issues/IssuesListModal";
|
||||
// icons
|
||||
import { UserIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
issuesList: IIssue[];
|
||||
customDisplay: JSX.Element;
|
||||
watchIssue: UseFormWatch<IIssue>;
|
||||
};
|
||||
|
||||
const SelectParent: React.FC<Props> = ({
|
||||
control,
|
||||
submitChanges,
|
||||
issuesList,
|
||||
customDisplay,
|
||||
watchIssue,
|
||||
}) => {
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
|
||||
const { activeProject, issues } = useUser();
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<UserIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>Parent</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssuesListModal
|
||||
isOpen={isParentModalOpen}
|
||||
handleClose={() => setIsParentModalOpen(false)}
|
||||
onChange={(val) => {
|
||||
submitChanges({ parent: val });
|
||||
onChange(val);
|
||||
}}
|
||||
issues={issuesList}
|
||||
title="Select Parent"
|
||||
value={value}
|
||||
customDisplay={customDisplay}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
onClick={() => setIsParentModalOpen(true)}
|
||||
>
|
||||
{watchIssue("parent") && watchIssue("parent") !== ""
|
||||
? `${activeProject?.identifier}-${
|
||||
issues?.results.find((i) => i.id === watchIssue("parent"))?.sequence_id
|
||||
}`
|
||||
: "Select Parent"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectParent;
|
@ -0,0 +1,84 @@
|
||||
// react
|
||||
import React from "react";
|
||||
// react-hook-form
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon, ChartBarIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { classNames } from "constants/common";
|
||||
import { PRIORITIES } from "constants/";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
};
|
||||
|
||||
const SelectPriority: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<ChartBarIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ priority: value });
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span className={classNames(value ? "" : "text-gray-900", "text-left")}>
|
||||
{value}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{PRIORITIES.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate capitalize`
|
||||
}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectPriority;
|
@ -0,0 +1,116 @@
|
||||
// react-hook-form
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { classNames } from "constants/common";
|
||||
import { Spinner } from "ui";
|
||||
import React from "react";
|
||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
};
|
||||
|
||||
const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
const { states } = useUser();
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<Squares2X2Icon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ state: value });
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"flex items-center gap-2 text-left"
|
||||
)}
|
||||
>
|
||||
{value ? (
|
||||
<>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: states?.find((option) => option.id === value)?.color,
|
||||
}}
|
||||
></span>
|
||||
{states?.find((option) => option.id === value)?.name}
|
||||
</>
|
||||
) : (
|
||||
"None"
|
||||
)}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{states ? (
|
||||
states.length > 0 ? (
|
||||
states.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.id}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||
}
|
||||
value={option.id}
|
||||
>
|
||||
{option.color && (
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: option.color }}
|
||||
></span>
|
||||
)}
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No states found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectState;
|
@ -82,9 +82,7 @@ const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
|
||||
|
||||
return (
|
||||
<div className="px-2">
|
||||
<div
|
||||
className={`relative ${sidebarCollapse ? "flex" : "grid grid-cols-5 gap-2 items-center"}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<Menu as="div" className="col-span-4 inline-block text-left w-full">
|
||||
<div className="w-full">
|
||||
<Menu.Button
|
||||
@ -102,14 +100,19 @@ const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
|
||||
alt="Workspace Logo"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
activeWorkspace?.name?.charAt(0) ?? "N"
|
||||
)}
|
||||
</div>
|
||||
{!sidebarCollapse && (
|
||||
<p className="truncate w-20 text-left ml-1">
|
||||
{activeWorkspace?.name ?? "Loading..."}
|
||||
<p className="text-left ml-1">
|
||||
{activeWorkspace?.name
|
||||
? activeWorkspace.name.length > 17
|
||||
? `${activeWorkspace.name.substring(0, 17)}...`
|
||||
: activeWorkspace.name
|
||||
: "Loading..."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -131,74 +134,124 @@ const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-left fixed max-w-[15rem] ml-2 left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||
<div className="p-1">
|
||||
{workspaces ? (
|
||||
<>
|
||||
{workspaces.length > 0 ? (
|
||||
workspaces.map((workspace: any) => (
|
||||
<Menu.Item key={workspace.id}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
mutateUser(
|
||||
(prevData) => ({
|
||||
...(prevData as IUser),
|
||||
last_workspace_id: workspace.id,
|
||||
}),
|
||||
false
|
||||
);
|
||||
userService
|
||||
.updateUser({
|
||||
last_workspace_id: workspace?.id,
|
||||
})
|
||||
.then((res) => {
|
||||
const isInProject = router.pathname.includes("/[projectId]/");
|
||||
if (isInProject) router.push("/workspace");
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}}
|
||||
className={`${
|
||||
active ? "bg-theme text-white" : "text-gray-900"
|
||||
} group flex w-full items-center rounded-md p-2 text-sm`}
|
||||
>
|
||||
{workspace.name}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))
|
||||
) : (
|
||||
<p>No workspace found!</p>
|
||||
)}
|
||||
<Menu.Item
|
||||
as="button"
|
||||
onClick={() => {
|
||||
router.push("/create-workspace");
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{({ active }) => (
|
||||
<a
|
||||
className={`flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm ${
|
||||
active ? "bg-theme text-white" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5" />
|
||||
<span>Create Workspace</span>
|
||||
</a>
|
||||
<div className="px-1 py-2 divide-y">
|
||||
<div>
|
||||
<Menu.Item as="div" className="text-xs px-2 pb-2">
|
||||
{user?.email}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
{workspaces ? (
|
||||
<>
|
||||
{workspaces.length > 0 ? (
|
||||
workspaces.map((workspace: any) => (
|
||||
<Menu.Item key={workspace.id}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
mutateUser(
|
||||
(prevData) => ({
|
||||
...(prevData as IUser),
|
||||
last_workspace_id: workspace.id,
|
||||
}),
|
||||
false
|
||||
);
|
||||
userService
|
||||
.updateUser({
|
||||
last_workspace_id: workspace?.id,
|
||||
})
|
||||
.then((res) => {
|
||||
const isInProject = router.pathname.includes("/[projectId]/");
|
||||
if (isInProject) router.push("/workspace");
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}}
|
||||
className={`${
|
||||
active ? "bg-indigo-50" : ""
|
||||
} w-full flex items-center gap-2 text-gray-900 rounded-md p-2 text-sm`}
|
||||
>
|
||||
<div className="h-5 w-5 p-4 flex items-center justify-center bg-gray-700 text-white rounded uppercase relative">
|
||||
{workspace?.logo && workspace.logo !== "" ? (
|
||||
<Image
|
||||
src={workspace.logo}
|
||||
alt="Workspace Logo"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
activeWorkspace?.name?.charAt(0) ?? "N"
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h5 className="text-sm">{workspace.name}</h5>
|
||||
<div className="text-xs text-gray-500">1 members</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))
|
||||
) : (
|
||||
<p>No workspace found!</p>
|
||||
)}
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
router.push("/create-workspace");
|
||||
}}
|
||||
className="w-full text-xs flex items-center gap-2 px-2 py-1 text-left rounded hover:bg-gray-100"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Create Workspace
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full flex justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs pt-2 space-y-1">
|
||||
{userLinks.map((link, index) => (
|
||||
<Menu.Item key={index} as="div">
|
||||
<Link href={link.href}>
|
||||
<a className="block px-2 py-1 text-left rounded hover:bg-gray-100">
|
||||
{link.name}
|
||||
</a>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full flex justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="w-full px-2 py-1 text-left rounded hover:bg-gray-100"
|
||||
onClick={async () => {
|
||||
await authenticationService
|
||||
.signOut({
|
||||
refresh_token: authenticationService.getRefreshToken(),
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("user signed out", response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Failed to sign out", error);
|
||||
})
|
||||
.finally(() => {
|
||||
mutateUser();
|
||||
router.push("/signin");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
{!sidebarCollapse && (
|
||||
{/* {!sidebarCollapse && (
|
||||
<Menu as="div" className="inline-block text-left flex-shrink-0 w-full">
|
||||
<div className="h-10 w-10">
|
||||
<Menu.Button className="h-full w-full grid relative place-items-center rounded-md shadow-sm bg-white text-gray-700 hover:bg-gray-50 focus:outline-none">
|
||||
@ -261,7 +314,7 @@ const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
<div className="mt-3 flex-1 space-y-1 bg-white">
|
||||
{workspaceLinks.map((link, index) => (
|
||||
|
@ -18,7 +18,9 @@ const AppLayout: React.FC<Props> = ({
|
||||
children,
|
||||
noPadding = false,
|
||||
bg = "primary",
|
||||
noHeader = false,
|
||||
breadcrumbs,
|
||||
left,
|
||||
right,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -37,7 +39,7 @@ const AppLayout: React.FC<Props> = ({
|
||||
<div className="h-screen w-full flex overflow-x-hidden">
|
||||
<Sidebar />
|
||||
<main className="h-screen w-full flex flex-col overflow-y-auto min-w-0">
|
||||
<Header breadcrumbs={breadcrumbs} right={right} />
|
||||
{noHeader ? null : <Header breadcrumbs={breadcrumbs} left={left} right={right} />}
|
||||
<div
|
||||
className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
|
||||
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
||||
|
2
apps/app/layouts/types.d.ts
vendored
2
apps/app/layouts/types.d.ts
vendored
@ -10,6 +10,8 @@ export type Props = {
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
noHeader?: boolean;
|
||||
breadcrumbs?: JSX.Element;
|
||||
left?: JSX.Element;
|
||||
right?: JSX.Element;
|
||||
};
|
||||
|
@ -1,45 +1,71 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
// next
|
||||
import type { NextPage } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// layouts
|
||||
import AppLayout from "layouts/app-layout";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// headless ui
|
||||
import { Disclosure, Listbox, Menu, Popover, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
||||
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
||||
import HeaderButton from "ui/HeaderButton";
|
||||
// constants
|
||||
import { USER_ISSUE } from "constants/fetch-keys";
|
||||
import { classNames } from "constants/common";
|
||||
import { Spinner, Breadcrumbs, BreadcrumbItem, EmptySpace, EmptySpaceItem, HeaderButton } from "ui";
|
||||
// icons
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
ChevronDownIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
PlusIcon,
|
||||
RectangleStackIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import User from "public/user.png";
|
||||
// services
|
||||
import userService from "lib/services/user.service";
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
// hooks
|
||||
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
||||
// hoc
|
||||
import withAuth from "lib/hoc/withAuthWrapper";
|
||||
// components
|
||||
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
||||
// icons
|
||||
import { ChevronDownIcon, PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import Link from "next/link";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { IIssue, IWorkspaceMember, Properties } from "types";
|
||||
// constants
|
||||
import { USER_ISSUE, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
classNames,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
replaceUnderscoreIfSnakeCase,
|
||||
} from "constants/common";
|
||||
import { PRIORITIES } from "constants/";
|
||||
|
||||
const MyIssues: NextPage = () => {
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null);
|
||||
const { activeWorkspace, user, states } = useUser();
|
||||
|
||||
const { user, workspaces, activeWorkspace } = useUser();
|
||||
console.log(states);
|
||||
|
||||
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
|
||||
user && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null,
|
||||
user && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
const [properties, setProperties] = useIssuesProperties(
|
||||
activeWorkspace?.slug,
|
||||
"21b5fab2-cb0c-4875-9496-619134bf1f32"
|
||||
);
|
||||
|
||||
const updateMyIssues = (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -79,98 +105,515 @@ const MyIssues: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
right={
|
||||
<HeaderButton
|
||||
Icon={PlusIcon}
|
||||
label="Add Issue"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "i",
|
||||
ctrlKey: true,
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
|
||||
"group flex gap-2 items-center rounded-md bg-transparent text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none border p-2"
|
||||
)}
|
||||
>
|
||||
<span>View</span>
|
||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{Object.keys(properties).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`px-2 py-1 capitalize rounded border border-theme text-xs ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-theme bg-theme text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<HeaderButton
|
||||
Icon={PlusIcon}
|
||||
label="Add Issue"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "i",
|
||||
ctrlKey: true,
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-full h-full flex flex-col space-y-5">
|
||||
{myIssues ? (
|
||||
<>
|
||||
{myIssues.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="overflow-x-auto ">
|
||||
<div className="inline-block min-w-full align-middle px-0.5 py-2">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-100">
|
||||
<tr className="text-left">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
|
||||
>
|
||||
NAME
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
|
||||
>
|
||||
DESCRIPTION
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
|
||||
>
|
||||
PROJECT
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
|
||||
>
|
||||
PRIORITY
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
|
||||
>
|
||||
STATUS
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white">
|
||||
{myIssues.map((myIssue, index) => (
|
||||
<tr
|
||||
key={myIssue.id}
|
||||
className={classNames(
|
||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||
"border-t text-sm text-gray-900"
|
||||
)}
|
||||
>
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
||||
<Link href={`/projects/${myIssue.project}/issues/${myIssue.id}`}>
|
||||
<a>{myIssue.name}</a>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-3 py-4 max-w-[15rem] truncate">
|
||||
{/* {myIssue.description} */}
|
||||
</td>
|
||||
<td className="px-3 py-4">
|
||||
{myIssue.project_detail?.name}
|
||||
<br />
|
||||
<span className="text-xs">{`(${myIssue.project_detail?.identifier}-${myIssue.sequence_id})`}</span>
|
||||
</td>
|
||||
<td className="px-3 py-4 capitalize">{myIssue.priority}</td>
|
||||
<td className="relative px-3 py-4">
|
||||
<ChangeStateDropdown
|
||||
issue={myIssue}
|
||||
updateIssues={updateMyIssues}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col space-y-5">
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-gray-500 ${
|
||||
!open ? "transform -rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
<h2 className="font-medium leading-5">My Issues</h2>
|
||||
<p className="text-gray-500 text-sm">{myIssues.length}</p>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="divide-y-2">
|
||||
{myIssues.map((issue: IIssue) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find(
|
||||
(p) => p.member.id === assignee
|
||||
)?.member;
|
||||
|
||||
return {
|
||||
avatar: tempPerson?.avatar,
|
||||
first_name: tempPerson?.first_name,
|
||||
email: tempPerson?.email,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{/* {properties.key && (
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)} */}
|
||||
<span className="">{issue.name}</span>
|
||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Name</h5>
|
||||
<div>{issue.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
{properties.priority && (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
// partialUpdateIssue({ priority: data }, issue.id);
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className={`rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer capitalize select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
{priority}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">
|
||||
Priority
|
||||
</h5>
|
||||
<div
|
||||
className={`capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "text-green-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
{properties.state && (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
// partialUpdateIssue({ state: data }, issue.id);
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
{states?.map((state) => (
|
||||
<Listbox.Option
|
||||
key={state.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={state.id}
|
||||
>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">State</h5>
|
||||
<div>{issue.state_detail.name}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
{properties.start_date && (
|
||||
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.start_date
|
||||
? renderShortNumericDateFormat(issue.start_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Started at</h5>
|
||||
<div>
|
||||
{renderShortNumericDateFormat(issue.start_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.target_date && (
|
||||
<div
|
||||
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 &&
|
||||
"text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">
|
||||
Target date
|
||||
</h5>
|
||||
<div>
|
||||
{renderShortNumericDateFormat(issue.target_date ?? "")}
|
||||
</div>
|
||||
<div>
|
||||
{issue.target_date &&
|
||||
(issue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: "Target date")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
if (newData.includes(data)) {
|
||||
newData.splice(newData.indexOf(data), 1);
|
||||
} else {
|
||||
newData.push(data);
|
||||
}
|
||||
// partialUpdateIssue({ assignees_list: newData }, issue.id);
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button>
|
||||
<div className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
{assignees.length > 0 ? (
|
||||
assignees.map((assignee, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
{assignee.avatar &&
|
||||
assignee.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={assignee.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={assignee?.first_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{assignee.first_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none p-2"
|
||||
)
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-x-1 ${
|
||||
assignees.includes({
|
||||
avatar: person.member.avatar,
|
||||
first_name: person.member.first_name,
|
||||
email: person.member.email,
|
||||
})
|
||||
? "font-medium"
|
||||
: "font-normal"
|
||||
}`}
|
||||
>
|
||||
{person.member.avatar &&
|
||||
person.member.avatar !== "" ? (
|
||||
<div className="relative h-4 w-4">
|
||||
<Image
|
||||
src={person.member.avatar}
|
||||
alt="avatar"
|
||||
className="rounded-full"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
|
||||
{person.member.first_name &&
|
||||
person.member.first_name !== ""
|
||||
? person.member.first_name.charAt(0)
|
||||
: person.member.email.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<p>
|
||||
{person.member.first_name &&
|
||||
person.member.first_name !== ""
|
||||
? person.member.first_name
|
||||
: person.member.email}
|
||||
</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Assigned to</h5>
|
||||
<div>
|
||||
{issue.assignee_details?.length > 0
|
||||
? issue.assignee_details
|
||||
.map((assignee) => assignee.first_name)
|
||||
.join(", ")
|
||||
: "No one"}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button
|
||||
as="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||
<Menu.Item>
|
||||
<button
|
||||
type="button"
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
onClick={() => {
|
||||
// setSelectedIssue({
|
||||
// ...issue,
|
||||
// actionType: "edit",
|
||||
// });
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||
<button
|
||||
type="button"
|
||||
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||
onClick={() => {
|
||||
// handleDeleteIssue(issue.id);
|
||||
}}
|
||||
>
|
||||
Delete permanently
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
||||
|
@ -1,6 +1,7 @@
|
||||
// react
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// swr
|
||||
import useSWR, { mutate } from "swr";
|
||||
@ -9,8 +10,8 @@ import { DropResult } from "react-beautiful-dnd";
|
||||
// layouots
|
||||
import AppLayout from "layouts/app-layout";
|
||||
// components
|
||||
import CyclesListView from "components/project/cycles/ListView";
|
||||
import CyclesBoardView from "components/project/cycles/BoardView";
|
||||
import CyclesListView from "components/project/cycles/list-view";
|
||||
import CyclesBoardView from "components/project/cycles/board-view";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
import cycleServices from "lib/services/cycles.service";
|
||||
@ -25,19 +26,21 @@ import { Menu, Popover, Transition } from "@headlessui/react";
|
||||
import { BreadcrumbItem, Breadcrumbs, CustomMenu } from "ui";
|
||||
// icons
|
||||
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ChevronDownIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
ListBulletIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ArrowPathIcon, ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { CycleIssueResponse, IIssue, NestedKeyOf, Properties } from "types";
|
||||
import {
|
||||
CycleIssueResponse,
|
||||
IIssue,
|
||||
NestedKeyOf,
|
||||
Properties,
|
||||
SelectIssue,
|
||||
SelectSprintType,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_ISSUES, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||
import Link from "next/link";
|
||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||
|
||||
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||
{ name: "State", key: "state_detail.name" },
|
||||
@ -73,6 +76,10 @@ const filterIssueOptions: Array<{
|
||||
type Props = {};
|
||||
|
||||
const SingleCycle: React.FC<Props> = () => {
|
||||
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
||||
const [selectedCycle, setSelectedCycle] = useState<SelectSprintType>();
|
||||
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
|
||||
|
||||
const { activeWorkspace, activeProject, cycles } = useUser();
|
||||
|
||||
const router = useRouter();
|
||||
@ -121,6 +128,21 @@ const SingleCycle: React.FC<Props> = () => {
|
||||
filterIssue,
|
||||
} = useIssuesFilter(cycleIssuesArray ?? []);
|
||||
|
||||
const openCreateIssueModal = (
|
||||
issue?: IIssue,
|
||||
actionType: "create" | "edit" | "delete" = "create"
|
||||
) => {
|
||||
const cycle = cycles?.find((cycle) => cycle.id === cycleId);
|
||||
if (cycle) {
|
||||
setSelectedCycle({
|
||||
...cycle,
|
||||
actionType: "create-issue",
|
||||
});
|
||||
if (issue) setSelectedIssues({ ...issue, actionType });
|
||||
setIsIssueModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const addIssueToCycle = (cycleId: string, issueId: string) => {
|
||||
if (!activeWorkspace || !activeProject?.id) return;
|
||||
|
||||
@ -200,18 +222,32 @@ const SingleCycle: React.FC<Props> = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${activeProject?.name ?? "Project"} Cycles`}
|
||||
link={`/projects/${activeProject?.id}/cycles`}
|
||||
/>
|
||||
{/* <BreadcrumbItem title={`${cycles?.find((c) => c.id === cycleId)?.name ?? "Cycle"} `} /> */}
|
||||
<>
|
||||
<CreateUpdateIssuesModal
|
||||
isOpen={
|
||||
isIssueModalOpen &&
|
||||
selectedCycle?.actionType === "create-issue" &&
|
||||
selectedIssues?.actionType !== "delete"
|
||||
}
|
||||
data={selectedIssues}
|
||||
prePopulateData={{ sprints: selectedCycle?.id }}
|
||||
setIsOpen={setIsIssueModalOpen}
|
||||
projectId={activeProject?.id}
|
||||
/>
|
||||
<AppLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${activeProject?.name ?? "Project"} Cycles`}
|
||||
link={`/projects/${activeProject?.id}/cycles`}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
left={
|
||||
<Menu as="div" className="relative inline-block">
|
||||
<Menu.Button className="flex items-center gap-1 border ml-3 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
|
||||
<Menu.Button className="flex items-center gap-1 border ml-2 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
|
||||
<ArrowPathIcon className="h-3 w-3" />
|
||||
Cycle
|
||||
{cycles?.find((c) => c.id === cycleId)?.name}
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
@ -240,176 +276,172 @@ const SingleCycle: React.FC<Props> = () => {
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
||||
issueView === "list" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIssueView("list");
|
||||
setGroupByProperty(null);
|
||||
}}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
||||
issueView === "kanban" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIssueView("kanban");
|
||||
setGroupByProperty("state_detail.name");
|
||||
}}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
|
||||
"group flex gap-2 items-center rounded-md bg-transparent text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none border p-2"
|
||||
)}
|
||||
>
|
||||
<span>View</span>
|
||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
}
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
||||
issueView === "list" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIssueView("list");
|
||||
setGroupByProperty(null);
|
||||
}}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
||||
issueView === "kanban" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIssueView("kanban");
|
||||
setGroupByProperty("state_detail.name");
|
||||
}}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
|
||||
"group flex gap-2 items-center rounded-md bg-transparent text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none border p-2"
|
||||
)}
|
||||
>
|
||||
<span>View</span>
|
||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByOptions.find((option) => option.key === groupByProperty)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{groupByOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{orderByOptions.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByOptions.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{groupByOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setOrderBy(option.key)}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm text-gray-600">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
filterIssueOptions.find((option) => option.key === filterIssue)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{filterIssueOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setFilterIssue(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="border-b-2"></div>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{Object.keys(properties).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`px-2 py-1 capitalize rounded border border-theme text-xs ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-theme bg-theme text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
))}
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{orderByOptions.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setOrderBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm text-gray-600">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
filterIssueOptions.find((option) => option.key === filterIssue)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{filterIssueOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setFilterIssue(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="border-b-2"></div>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{Object.keys(properties).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`px-2 py-1 capitalize rounded border border-theme text-xs ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-theme bg-theme text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{issueView === "list" ? (
|
||||
<CyclesListView
|
||||
groupedByIssues={groupedByIssues}
|
||||
selectedGroup={groupByProperty}
|
||||
properties={properties}
|
||||
openCreateIssueModal={() => {
|
||||
return;
|
||||
}}
|
||||
openIssuesListModal={() => {
|
||||
return;
|
||||
}}
|
||||
removeIssueFromCycle={removeIssueFromCycle}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-screen">
|
||||
<CyclesBoardView
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{issueView === "list" ? (
|
||||
<CyclesListView
|
||||
groupedByIssues={groupedByIssues}
|
||||
properties={properties}
|
||||
removeIssueFromCycle={removeIssueFromCycle}
|
||||
selectedGroup={groupByProperty}
|
||||
members={members}
|
||||
openCreateIssueModal={() => {
|
||||
return;
|
||||
}}
|
||||
properties={properties}
|
||||
openCreateIssueModal={openCreateIssueModal}
|
||||
openIssuesListModal={() => {
|
||||
return;
|
||||
}}
|
||||
removeIssueFromCycle={removeIssueFromCycle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AppLayout>
|
||||
) : (
|
||||
<div className="h-screen">
|
||||
<CyclesBoardView
|
||||
groupedByIssues={groupedByIssues}
|
||||
properties={properties}
|
||||
removeIssueFromCycle={removeIssueFromCycle}
|
||||
selectedGroup={groupByProperty}
|
||||
members={members}
|
||||
openCreateIssueModal={openCreateIssueModal}
|
||||
openIssuesListModal={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AppLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,11 +3,9 @@ import React, { useEffect, useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
import type { NextPage } from "next";
|
||||
import Link from "next/link";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
import sprintService from "lib/services/cycles.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
@ -24,6 +22,7 @@ import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deleti
|
||||
import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion";
|
||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||
import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal";
|
||||
import CycleStatsView from "components/project/cycles/stats-view";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
@ -204,11 +203,7 @@ const ProjectSprints: NextPage = () => {
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
<div className="space-y-5">
|
||||
{cycles.map((cycle) => (
|
||||
<Link key={cycle.id} href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
|
||||
<a className="block bg-white p-3 rounded-md">{cycle.name}</a>
|
||||
</Link>
|
||||
))}
|
||||
<CycleStatsView cycles={cycles} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
||||
|
@ -1,4 +1,5 @@
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import type { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import dynamic from "next/dynamic";
|
||||
@ -27,10 +28,12 @@ import AppLayout from "layouts/app-layout";
|
||||
// components
|
||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
|
||||
import AddAsSubIssue from "components/command-palette/addAsSubIssue";
|
||||
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
||||
// common
|
||||
import { debounce } from "constants/common";
|
||||
// components
|
||||
import IssueDetailSidebar from "components/project/issues/issue-detail/IssueDetailSidebar";
|
||||
import IssueDetailSidebar from "components/project/issues/issue-detail/issue-detail-sidebar";
|
||||
// activites
|
||||
import IssueActivitySection from "components/project/issues/issue-detail/activity";
|
||||
// ui
|
||||
@ -46,9 +49,6 @@ import {
|
||||
EllipsisHorizontalIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import AddAsSubIssue from "components/command-palette/addAsSubIssue";
|
||||
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
||||
|
||||
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
|
||||
ssr: false,
|
||||
|
@ -13,6 +13,7 @@ import { Tab } from "@headlessui/react";
|
||||
import withAuth from "lib/hoc/withAuthWrapper";
|
||||
// layouts
|
||||
import SettingsLayout from "layouts/settings-layout";
|
||||
import AppLayout from "layouts/app-layout";
|
||||
// service
|
||||
import projectServices from "lib/services/project.service";
|
||||
// hooks
|
||||
@ -155,14 +156,14 @@ const ProjectSettings: NextPage = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
<AppLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link="/projects" />
|
||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
links={sidebarLinks}
|
||||
// links={sidebarLinks}
|
||||
>
|
||||
{projectDetails ? (
|
||||
<div className="space-y-3">
|
||||
@ -209,7 +210,7 @@ const ProjectSettings: NextPage = () => {
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</SettingsLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -18,9 +18,14 @@ import userService from "lib/services/user.service";
|
||||
// ui
|
||||
import { Spinner } from "ui";
|
||||
// icons
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
import { ArrowRightIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
|
||||
const Workspace: NextPage = () => {
|
||||
const { user, activeWorkspace, projects } = useUser();
|
||||
@ -46,7 +51,7 @@ const Workspace: NextPage = () => {
|
||||
const hours = new Date().getHours();
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppLayout noHeader={true}>
|
||||
<div className="h-full w-full space-y-5">
|
||||
{user ? (
|
||||
<div className="font-medium text-2xl">
|
||||
@ -78,49 +83,105 @@ const Workspace: NextPage = () => {
|
||||
<div className="max-h-[30rem] overflow-y-auto w-full border border-gray-200 bg-white rounded-lg shadow-sm col-span-2">
|
||||
{myIssues ? (
|
||||
myIssues.length > 0 ? (
|
||||
<table className="h-full w-full overflow-y-auto">
|
||||
<thead className="border-b bg-gray-50 text-sm">
|
||||
<tr>
|
||||
<th scope="col" className="px-3 py-4 text-left">
|
||||
ISSUE
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-4 text-left">
|
||||
KEY
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-4 text-left">
|
||||
STATUS
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{myIssues?.map((issue, index) => (
|
||||
<tr
|
||||
className="border-t transition duration-300 ease-in-out hover:bg-gray-100 text-gray-900 gap-3 text-sm"
|
||||
key={index}
|
||||
>
|
||||
<td className="px-3 py-4 font-medium">
|
||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-3 py-4">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</td>
|
||||
<td className="px-3 py-4">
|
||||
<span
|
||||
className="rounded px-2 py-1 text-xs font-medium"
|
||||
style={{
|
||||
border: `2px solid ${issue.state_detail.color}`,
|
||||
backgroundColor: `${issue.state_detail.color}20`,
|
||||
}}
|
||||
>
|
||||
{issue.state_detail.name ?? "None"}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col space-y-5">
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h2 className="font-medium leading-5">My Issues</h2>
|
||||
<p className="text-gray-500 text-sm">{myIssues.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y-2">
|
||||
{myIssues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{/* {properties.key && (
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)} */}
|
||||
<span className="">{issue.name}</span>
|
||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">Name</h5>
|
||||
<div>{issue.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
<div
|
||||
className={`rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</div>
|
||||
<div
|
||||
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 &&
|
||||
"text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "N/A"}
|
||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1 text-gray-900">Target date</h5>
|
||||
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
|
||||
<div>
|
||||
{issue.target_date &&
|
||||
(issue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
issue.target_date
|
||||
)} days`
|
||||
: "Target date")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-10">
|
||||
<p className="text-gray-500 text-center">No Issues Found</p>
|
||||
|
Loading…
Reference in New Issue
Block a user