fix: bug fixes in cycles
This commit is contained in:
parent
1c30e53ef9
commit
0cda15408d
23 changed files with 2550 additions and 2052 deletions
|
|
@ -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…
Add table
Add a link
Reference in a new issue