resolved merge conflict

This commit is contained in:
Dakshesh Jain 2022-12-17 19:31:25 +05:30
commit eb175ba823
64 changed files with 4347 additions and 3562 deletions

View file

@ -3,56 +3,71 @@ import React from "react";
// next
import Link from "next/link";
import type { NextPage } from "next";
import Image from "next/image";
// swr
import useSWR from "swr";
// headless ui
import { Transition, Popover } from "@headlessui/react";
import { Disclosure, Listbox, Menu, Popover, Transition } from "@headlessui/react";
// layouts
import AppLayout from "layouts/app-layout";
// hooks
import useUser from "lib/hooks/useUser";
// headless ui
// ui
import { Spinner, Breadcrumbs, BreadcrumbItem, EmptySpace, EmptySpaceItem, HeaderButton } from "ui";
// icons
import {
Spinner,
HeaderButton,
EmptySpace,
EmptySpaceItem,
Breadcrumbs,
BreadcrumbItem,
CustomMenu,
} from "ui";
// constants
import { USER_ISSUE } from "constants/fetch-keys";
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
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";
import useMyIssuesProperties from "lib/hooks/useMyIssueFilter";
// components
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
// icons
import { ChevronDownIcon, PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, NestedKeyOf, Properties } from "types";
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" },
{ name: "Priority", key: "priority" },
{ name: "Cycle", key: "issue_cycle.cycle_detail.name" },
{ name: "Created By", key: "created_by" },
{ name: "None", key: null },
];
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 { user, activeWorkspace } = useUser();
const { activeWorkspace, user, states } = 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,
@ -84,13 +99,8 @@ const MyIssues: NextPage = () => {
});
};
const {
filteredIssues,
properties,
setMyIssueGroupByProperty,
setMyIssueProperty,
groupByProperty,
} = useMyIssuesProperties(myIssues);
const { filteredIssues, setMyIssueGroupByProperty, setMyIssueProperty, groupByProperty } =
useMyIssuesProperties(myIssues);
return (
<AppLayout
@ -100,7 +110,7 @@ const MyIssues: NextPage = () => {
</Breadcrumbs>
}
right={
<>
<div className="flex items-center gap-2">
<Popover className="relative">
{({ open }) => (
<>
@ -123,27 +133,8 @@ const MyIssues: NextPage = () => {
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">
<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={() => setMyIssueGroupByProperty(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">
@ -156,7 +147,7 @@ const MyIssues: NextPage = () => {
? "border-theme bg-theme text-white"
: ""
}`}
onClick={() => setMyIssueProperty(key as keyof Properties)}
onClick={() => setProperties(key as keyof Properties)}
>
{replaceUnderscoreIfSnakeCase(key)}
</button>
@ -180,88 +171,454 @@ const MyIssues: NextPage = () => {
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">

View file

@ -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";
@ -20,24 +21,28 @@ import useUser from "lib/hooks/useUser";
import useIssuesFilter from "lib/hooks/useIssuesFilter";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// headless ui
import { Menu, Popover, Transition } from "@headlessui/react";
import { Popover, Transition } from "@headlessui/react";
// ui
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";
import CycleIssuesListModal from "components/project/cycles/cycle-issues-list-modal";
import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion";
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" },
@ -73,7 +78,12 @@ const filterIssueOptions: Array<{
type Props = {};
const SingleCycle: React.FC<Props> = () => {
const { activeWorkspace, activeProject, cycles } = useUser();
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [selectedCycle, setSelectedCycle] = useState<SelectSprintType>();
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const { activeWorkspace, activeProject, cycles, issues } = useUser();
const router = useRouter();
@ -91,9 +101,8 @@ const SingleCycle: React.FC<Props> = () => {
cycleServices.getCycleIssues(activeWorkspace?.slug, activeProject?.id, cycleId as string)
: null
);
const cycleIssuesArray = cycleIssues?.map((issue) => {
return issue.issue_details;
return { bridge: issue.id, ...issue.issue_details };
});
const { data: members } = useSWR(
@ -121,6 +130,25 @@ 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 openIssuesListModal = () => {
setCycleIssuesListModal(true);
};
const addIssueToCycle = (cycleId: string, issueId: string) => {
if (!activeWorkspace || !activeProject?.id) return;
@ -180,16 +208,16 @@ const SingleCycle: React.FC<Props> = () => {
// console.log(result);
};
const removeIssueFromCycle = (cycleId: string, bridgeId: string) => {
const removeIssueFromCycle = (bridgeId: string) => {
if (activeWorkspace && activeProject) {
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId),
CYCLE_ISSUES(cycleId as string),
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
false
);
issuesServices
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId)
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId as string, bridgeId)
.then((res) => {
console.log(res);
})
@ -200,216 +228,218 @@ 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"} `} /> */}
<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">
<ArrowPathIcon className="h-3 w-3" />
Cycle
</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-3 mt-2 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
{cycles?.map((cycle) => (
<Menu.Item key={cycle.id}>
<Link href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
<a
className={`block text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full ${
cycle.id === cycleId ? "bg-theme text-white" : ""
}`}
>
{cycle.name}
</a>
</Link>
</Menu.Item>
))}
</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 }) => (
<>
<CreateUpdateIssuesModal
isOpen={
isIssueModalOpen &&
selectedCycle?.actionType === "create-issue" &&
selectedIssues?.actionType !== "delete"
}
data={selectedIssues}
prePopulateData={{ sprints: selectedCycle?.id }}
setIsOpen={setIsIssueModalOpen}
projectId={activeProject?.id}
/>
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycleId as string}
/>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Cycles`}
link={`/projects/${activeProject?.id}/cycles`}
/>
</Breadcrumbs>
}
left={
<CustomMenu
label={
<>
<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>
<ArrowPathIcon className="h-3 w-3" />
{cycles?.find((c) => c.id === cycleId)?.name}
</>
}
className="ml-1.5"
>
{cycles?.map((cycle) => (
<CustomMenu.MenuItem
key={cycle.id}
renderAs="a"
href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}
>
{cycle.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}
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 right-0 z-10 mt-1 w-screen max-w-xs 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"
}
width="auto"
>
{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"
}
width="auto"
>
{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"
}
width="auto"
>
{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;
}}
openIssuesListModal={() => {
return;
}}
properties={properties}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={openIssuesListModal}
removeIssueFromCycle={removeIssueFromCycle}
/>
</div>
)}
</AppLayout>
) : (
<div className="h-screen">
<CyclesBoardView
groupedByIssues={groupedByIssues}
properties={properties}
removeIssueFromCycle={removeIssueFromCycle}
selectedGroup={groupByProperty}
members={members}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={openIssuesListModal}
/>
</div>
)}
</AppLayout>
</>
);
};

View file

@ -3,49 +3,33 @@ 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";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// services
import issuesServices from "lib/services/issues.service";
import sprintService from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// fetching keys
import { CYCLE_LIST } from "constants/fetch-keys";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/app-layout";
// components
import CycleIssuesListModal from "components/project/cycles/CycleIssuesListModal";
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal";
// headless ui
import { Popover, Transition } from "@headlessui/react";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
import CycleStatsView from "components/project/cycles/stats-view";
// ui
import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySpaceItem } from "ui";
// icons
import { ArrowPathIcon, ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
import { ArrowPathIcon, PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, ICycle, SelectSprintType, SelectIssue, Properties } from "types";
// constants
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
import { ICycle, SelectSprintType } from "types";
// fetching keys
import { CYCLE_LIST } from "constants/fetch-keys";
const ProjectSprints: NextPage = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedSprint, setSelectedSprint] = useState<SelectSprintType>();
const [selectedCycle, setSelectedCycle] = useState<SelectSprintType>();
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const [cycleId, setCycleId] = useState("");
const { activeWorkspace, activeProject, issues } = useUser();
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
@ -58,45 +42,13 @@ const ProjectSprints: NextPage = () => {
: null
);
const [properties, setProperties] = useIssuesProperties(
activeWorkspace?.slug,
projectId as string
);
const openCreateIssueModal = (
cycleId: string,
issue?: IIssue,
actionType: "create" | "edit" | "delete" = "create"
) => {
const cycle = cycles?.find((cycle) => cycle.id === cycleId);
if (cycle) {
setSelectedSprint({
...cycle,
actionType: "create-issue",
});
if (issue) setSelectedIssues({ ...issue, actionType });
setIsIssueModalOpen(true);
}
};
const openIssuesListModal = (cycleId: string) => {
setCycleId(cycleId);
setCycleIssuesListModal(true);
};
useEffect(() => {
if (isOpen) return;
if (createUpdateCycleModal) return;
const timer = setTimeout(() => {
setSelectedSprint(undefined);
setSelectedCycle(undefined);
clearTimeout(timer);
}, 500);
}, [isOpen]);
useEffect(() => {
if (selectedIssues?.actionType === "delete") {
setDeleteIssue(selectedIssues.id);
}
}, [selectedIssues]);
}, [createUpdateCycleModal]);
return (
<AppLayout
@ -110,105 +62,33 @@ const ProjectSprints: NextPage = () => {
</Breadcrumbs>
}
right={
<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-4 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>
{Object.keys(properties).map((key) => (
<button
key={key}
type="button"
className={`px-2 py-1 inline capitalize rounded border border-theme text-sm m-1 ${
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 Cycle" onClick={() => setIsOpen(true)} />
</div>
<HeaderButton
Icon={PlusIcon}
label="Add Cycle"
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "q",
});
document.dispatchEvent(e);
}}
/>
}
>
<CreateUpdateSprintsModal
isOpen={
isOpen &&
selectedSprint?.actionType !== "delete" &&
selectedSprint?.actionType !== "create-issue"
}
setIsOpen={setIsOpen}
data={selectedSprint}
<CreateUpdateCycleModal
isOpen={createUpdateCycleModal}
setIsOpen={setCreateUpdateCycleModal}
projectId={projectId as string}
/>
<ConfirmSprintDeletion
isOpen={isOpen && !!selectedSprint && selectedSprint.actionType === "delete"}
setIsOpen={setIsOpen}
data={selectedSprint}
/>
<ConfirmIssueDeletion
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={selectedIssues}
/>
<CreateUpdateIssuesModal
isOpen={
isIssueModalOpen &&
selectedSprint?.actionType === "create-issue" &&
selectedIssues?.actionType !== "delete"
}
data={selectedIssues}
prePopulateData={{ sprints: selectedSprint?.id }}
setIsOpen={setIsOpen}
projectId={projectId as string}
/>
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycleId}
data={selectedCycle}
/>
{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}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
/>
</div>
) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4">
@ -226,7 +106,7 @@ const ProjectSprints: NextPage = () => {
</span>
}
Icon={PlusIcon}
action={() => setIsOpen(true)}
action={() => setCreateUpdateCycleModal(true)}
/>
</EmptySpace>
</div>

View file

@ -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";
@ -7,7 +8,7 @@ import React, { useCallback, useEffect, useState } from "react";
// swr
import useSWR, { mutate } from "swr";
// react hook form
import { useForm, Controller } from "react-hook-form";
import { useForm } from "react-hook-form";
// headless ui
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
// services
@ -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/project/issues/issue-detail/add-as-sub-issue";
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,

View file

@ -220,6 +220,7 @@ const ProjectIssues: NextPage = () => {
groupByOptions.find((option) => option.key === groupByProperty)?.name ??
"Select"
}
width="auto"
>
{groupByOptions.map((option) => (
<CustomMenu.MenuItem
@ -238,6 +239,7 @@ const ProjectIssues: NextPage = () => {
orderByOptions.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="auto"
>
{orderByOptions.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
@ -258,6 +260,7 @@ const ProjectIssues: NextPage = () => {
filterIssueOptions.find((option) => option.key === filterIssue)?.name ??
"Select"
}
width="auto"
>
{filterIssueOptions.map((option) => (
<CustomMenu.MenuItem

View file

@ -0,0 +1,104 @@
// next
import type { NextPage } from "next";
import useSWR from "swr";
import { useRouter } from "next/router";
// layouts
import AppLayout from "layouts/app-layout";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// services
import modulesService from "lib/services/modules.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { BreadcrumbItem, Breadcrumbs, EmptySpace, EmptySpaceItem, HeaderButton, Spinner } from "ui";
// icons
import { PlusIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
// types
import { IModule } from "types/modules";
// fetch-keys
import { MODULE_LIST } from "constants/fetch-keys";
const ProjectModules: NextPage = () => {
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
const { projectId } = router.query;
const { data: modules } = useSWR<IModule[]>(
activeWorkspace && projectId ? MODULE_LIST(projectId as string) : null,
activeWorkspace && projectId
? () => modulesService.getModules(activeWorkspace.slug, projectId as string)
: null
);
console.log(modules);
return (
<AppLayout
meta={{
title: "Plane - Modules",
}}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Modules`} />
</Breadcrumbs>
}
right={
<HeaderButton
Icon={PlusIcon}
label="Add Module"
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "m",
});
document.dispatchEvent(e);
}}
/>
}
>
{modules ? (
modules.length > 0 ? (
<div className="space-y-5">
{modules.map((module) => (
<div key={module.id} className="bg-white p-3 rounded-md">
<h3>{module.name}</h3>
<p className="text-gray-500 text-sm mt-2">{module.description}</p>
</div>
))}
</div>
) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4">
<EmptySpace
title="You don't have any module yet."
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
Icon={RectangleGroupIcon}
>
<EmptySpaceItem
title="Create a new module"
description={
<span>
Use <pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + Q</pre>{" "}
shortcut to create a new cycle
</span>
}
Icon={PlusIcon}
action={() => {
return;
}}
/>
</EmptySpace>
</div>
)
) : (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
)}
</AppLayout>
);
};
export default withAuth(ProjectModules);

View file

@ -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>
);
};

View file

@ -57,7 +57,23 @@ const Projects: NextPage = () => {
};
return (
<AppLayout>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} />
</Breadcrumbs>
}
right={
<HeaderButton
Icon={PlusIcon}
label="Add Project"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
document.dispatchEvent(e);
}}
/>
}
>
<ConfirmProjectDeletion
isOpen={!!deleteProject}
onClose={() => setDeleteProject(null)}
@ -91,20 +107,6 @@ const Projects: NextPage = () => {
</div>
) : (
<div className="h-full w-full space-y-5">
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} />
</Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Projects</h2>
<HeaderButton
Icon={PlusIcon}
label="Add Project"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
document.dispatchEvent(e);
}}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((item) => (
<ProjectMemberInvitations

View file

@ -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>