Merge branch 'stage-release' of https://github.com/makeplane/plane

This commit is contained in:
Dakshesh Jain 2022-12-14 17:48:37 +05:30
commit 919c226f8a
65 changed files with 4986 additions and 2130 deletions

View File

@ -88,9 +88,7 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
<div className="flex items-center justify-between mt-2">
<div className="text-sm ml-auto">
<Link href={"/forgot-password"}>
<a className="font-medium text-indigo-600 hover:text-indigo-500">
Forgot your password?
</a>
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a>
</Link>
</div>
</div>

View File

@ -27,7 +27,7 @@ import { getValidatedValue } from "./helpers/editor";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
export interface RichTextEditorProps {
onChange: (state: SerializedEditorState) => void;
onChange: (state: string) => void;
id: string;
value: string;
placeholder?: string;
@ -41,8 +41,7 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
}) => {
const handleChange = (editorState: EditorState) => {
editorState.read(() => {
let editorData = editorState.toJSON();
if (onChange) onChange(editorData);
onChange(JSON.stringify(editorState.toJSON()));
});
};

View File

@ -18,10 +18,12 @@ export const getValidatedValue = (value: string) => {
const defaultValue =
'{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
console.log("Value: ", value);
if (value) {
try {
console.log(value);
return value;
const data = JSON.parse(value);
return JSON.stringify(data);
} catch (e) {
return defaultValue;
}

View File

@ -216,7 +216,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
{selected ? (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active ? "text-white" : "text-indigo-600"
active ? "text-white" : "text-theme"
}`}
>
<CheckIcon

View File

@ -130,15 +130,6 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
const projectName = watch("name") ?? "";
const projectIdentifier = watch("identifier") ?? "";
if (workspaceMembers) {
const isMember = workspaceMembers.find((member) => member.member.id === user?.id);
const isGuest = workspaceMembers.find(
(member) => member.member.id === user?.id && member.role === 5
);
if ((!isMember || isGuest) && isOpen) return <IsGuestCondition setIsOpen={setIsOpen} />;
}
useEffect(() => {
if (projectName && isChangeIdentifierRequired) {
setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3));
@ -157,12 +148,21 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101),
projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101),
]);
}, [errors.identifier]);
}, [errors.identifier, projectIdentifier, projectName]);
useEffect(() => {
return () => setIsChangeIdentifierRequired(true);
}, [isOpen]);
if (workspaceMembers) {
const isMember = workspaceMembers.find((member) => member.member.id === user?.id);
const isGuest = workspaceMembers.find(
(member) => member.member.id === user?.id && member.role === 5
);
if ((!isMember || isGuest) && isOpen) return <IsGuestCondition setIsOpen={setIsOpen} />;
}
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" onClose={handleClose}>

View File

@ -0,0 +1,79 @@
// components
import SingleBoard from "components/project/cycles/BoardView/single-board";
// ui
import { Spinner } from "ui";
// types
import { IIssue, IProjectMember, NestedKeyOf, Properties } from "types";
import useUser from "lib/hooks/useUser";
type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
properties: Properties;
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
openCreateIssueModal: (
sprintId: string,
issue?: IIssue,
actionType?: "create" | "edit" | "delete"
) => void;
openIssuesListModal: (cycleId: string) => void;
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
};
const CyclesBoardView: React.FC<Props> = ({
groupedByIssues,
properties,
selectedGroup,
members,
openCreateIssueModal,
openIssuesListModal,
removeIssueFromCycle,
}) => {
const { states } = useUser();
return (
<>
{groupedByIssues ? (
<div className="h-full w-full">
<div className="h-full w-full overflow-hidden">
<div className="h-full w-full">
<div className="flex gap-x-4 h-full overflow-x-auto overflow-y-hidden pb-3">
{Object.keys(groupedByIssues).map((singleGroup) => (
<SingleBoard
key={singleGroup}
selectedGroup={selectedGroup}
groupTitle={singleGroup}
createdBy={
selectedGroup === "created_by"
? members?.find((m) => m.member.id === singleGroup)?.member.first_name ??
"loading..."
: null
}
groupedByIssues={groupedByIssues}
bgColor={
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.color
: undefined
}
properties={properties}
removeIssueFromCycle={removeIssueFromCycle}
openIssuesListModal={openIssuesListModal}
openCreateIssueModal={openCreateIssueModal}
/>
))}
</div>
</div>
</div>
</div>
) : (
<div className="h-full w-full flex justify-center items-center">
<Spinner />
</div>
)}
</>
);
};
export default CyclesBoardView;

View File

@ -0,0 +1,662 @@
// react
import React, { useState } from "react";
// next
import Link from "next/link";
import Image from "next/image";
// swr
import useSWR from "swr";
// services
import cycleServices from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Spinner } from "ui";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
CalendarDaysIcon,
PlusIcon,
EllipsisHorizontalIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import User from "public/user.png";
// types
import {
CycleIssueResponse,
ICycle,
IIssue,
IWorkspaceMember,
NestedKeyOf,
Properties,
} from "types";
// constants
import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys";
import {
addSpaceIfCamelCase,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
import { Menu, Transition } from "@headlessui/react";
import workspaceService from "lib/services/workspace.service";
type Props = {
properties: Properties;
groupedByIssues: {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
createdBy: string | null;
bgColor?: string;
openCreateIssueModal: (
sprintId: string,
issue?: IIssue,
actionType?: "create" | "edit" | "delete"
) => void;
openIssuesListModal: (cycleId: string) => void;
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
};
const SingleCycleBoard: React.FC<Props> = ({
properties,
groupedByIssues,
selectedGroup,
groupTitle,
createdBy,
bgColor,
openCreateIssueModal,
openIssuesListModal,
removeIssueFromCycle,
}) => {
// Collapse/Expand
const [show, setState] = useState(true);
const { activeWorkspace, activeProject } = useUser();
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
const { data: people } = useSWR<IWorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
);
return (
<div className={`rounded flex-shrink-0 h-full ${!show ? "" : "w-80 bg-gray-50 border"}`}>
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
<div
className={`flex justify-between p-3 pb-0 ${
!show ? "flex-col bg-gray-50 rounded-md border" : ""
}`}
>
<div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
<div
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
}`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
>
<span
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
style={{
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
}}
/>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !show ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="text-gray-500 text-sm ml-0.5">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
</div>
<div
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
!show ? "hidden" : "block"
}`}
>
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
const assignees = [
...(childIssue?.assignees_list ?? []),
...(childIssue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
});
return (
<div key={childIssue.id} className={`border rounded bg-white shadow-sm`}>
<div className="group/card relative p-2 select-none">
<div className="opacity-0 group-hover/card:opacity-100 absolute top-1 right-1">
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded text-red-500 hover:bg-red-50 duration-300 outline-none"
// onClick={() => handleDeleteIssue(childIssue.id)}
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
<a>
{properties.key && (
<div className="text-xs font-medium text-gray-500 mb-2">
{activeProject?.identifier}-{childIssue.sequence_id}
</div>
)}
<h5
className="group-hover:text-theme text-sm break-all mb-3"
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
>
{childIssue.name}
</h5>
</a>
</Link>
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
{properties.priority && (
<div
className={`group flex-shrink-0 flex items-center gap-1 rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
childIssue.priority === "urgent"
? "bg-red-100 text-red-600"
: childIssue.priority === "high"
? "bg-orange-100 text-orange-500"
: childIssue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: childIssue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{/* {getPriorityIcon(childIssue.priority ?? "")} */}
{childIssue.priority ?? "None"}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
<div
className={`capitalize ${
childIssue.priority === "urgent"
? "text-red-600"
: childIssue.priority === "high"
? "text-orange-500"
: childIssue.priority === "medium"
? "text-yellow-500"
: childIssue.priority === "low"
? "text-green-500"
: ""
}`}
>
{childIssue.priority ?? "None"}
</div>
</div>
</div>
)}
{properties.state && (
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: childIssue.state_detail.color }}
></span>
{addSpaceIfCamelCase(childIssue.state_detail.name)}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">State</h5>
<div>{childIssue.state_detail.name}</div>
</div>
</div>
)}
{properties.start_date && (
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<CalendarDaysIcon className="h-4 w-4" />
{childIssue.start_date
? renderShortNumericDateFormat(childIssue.start_date)
: "N/A"}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">Started at</h5>
<div>{renderShortNumericDateFormat(childIssue.start_date ?? "")}</div>
</div>
</div>
)}
{properties.target_date && (
<div
className={`group flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
childIssue.target_date === null
? ""
: childIssue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(childIssue.target_date) <= 3 && "text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{childIssue.target_date
? renderShortNumericDateFormat(childIssue.target_date)
: "N/A"}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">Target date</h5>
<div>{renderShortNumericDateFormat(childIssue.target_date ?? "")}</div>
<div>
{childIssue.target_date &&
(childIssue.target_date < new Date().toISOString()
? `Target date has passed by ${findHowManyDaysLeft(
childIssue.target_date
)} days`
: findHowManyDaysLeft(childIssue.target_date) <= 3
? `Target date is in ${findHowManyDaysLeft(
childIssue.target_date
)} days`
: "Target date")}
</div>
</div>
</div>
)}
{properties.assignee && (
<div className="group flex items-center gap-1 text-xs">
{childIssue.assignee_details?.length > 0 ? (
childIssue.assignee_details?.map((assignee, index: number) => (
<div
key={index}
className={`relative z-[1] h-5 w-5 rounded-full ${
index !== 0 ? "-ml-2.5" : ""
}`}
>
{assignee.avatar && assignee.avatar !== "" ? (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<Image
src={assignee.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={assignee.name}
/>
</div>
) : (
<div
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
>
{assignee.first_name.charAt(0)}
</div>
)}
</div>
))
) : (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<Image
src={User}
height="100%"
width="100%"
className="rounded-full"
alt="No user"
/>
</div>
)}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">Assigned to</h5>
<div>
{childIssue.assignee_details?.length > 0
? childIssue.assignee_details
.map((assignee) => assignee.first_name)
.join(", ")
: "No one"}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
})}
<button
type="button"
className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
>
<PlusIcon className="h-3 w-3 mr-1" />
Create
</button>
</div>
</div>
</div>
);
// return (
// <div className={`rounded flex-shrink-0 h-full ${!show ? "" : "w-80 bg-gray-50 border"}`}>
// <div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
// <div
// className={`flex justify-between p-3 pb-0 ${
// !show ? "flex-col bg-gray-50 rounded-md border" : ""
// }`}
// >
// <div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
// <div
// className={`flex items-center gap-x-1 rounded-md cursor-pointer ${
// !show ? "py-2 mb-2 flex-col gap-y-2" : ""
// }`}
// >
// <h2
// className={`text-[0.9rem] font-medium capitalize`}
// style={{
// writingMode: !show ? "vertical-rl" : "horizontal-tb",
// }}
// >
// {cycle.name}
// </h2>
// <span className="text-gray-500 text-sm ml-0.5">{cycleIssues?.length}</span>
// </div>
// </div>
// <div className={`flex items-center ${!show ? "flex-col pb-2" : ""}`}>
// <button
// type="button"
// className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
// onClick={() => {
// setState(!show);
// }}
// >
// {show ? (
// <ArrowsPointingInIcon className="h-4 w-4" />
// ) : (
// <ArrowsPointingOutIcon className="h-4 w-4" />
// )}
// </button>
// <Menu as="div" className="relative inline-block">
// <Menu.Button className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none">
// <EllipsisHorizontalIcon className="h-4 w-4" />
// </Menu.Button>
// <Transition
// as={React.Fragment}
// enter="transition ease-out duration-100"
// enterFrom="transform opacity-0 scale-95"
// enterTo="transform opacity-100 scale-100"
// leave="transition ease-in duration-75"
// leaveFrom="transform opacity-100 scale-100"
// leaveTo="transform opacity-0 scale-95"
// >
// <Menu.Items className="absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
// <div className="p-1">
// <Menu.Item as="div">
// {(active) => (
// <button
// type="button"
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// onClick={() => openCreateIssueModal(cycle.id)}
// >
// Create new
// </button>
// )}
// </Menu.Item>
// <Menu.Item as="div">
// {(active) => (
// <button
// type="button"
// className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
// onClick={() => openIssuesListModal(cycle.id)}
// >
// Add an existing issue
// </button>
// )}
// </Menu.Item>
// </div>
// </Menu.Items>
// </Transition>
// </Menu>
// </div>
// </div>
// <div
// className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
// !show ? "hidden" : "block"
// }`}
// >
// {cycleIssues ? (
// cycleIssues.map((issue, index: number) => (
// <div key={childIssue.id} className="border rounded bg-white shadow-sm">
// <div className="group/card relative p-2 select-none">
// <div className="opacity-0 group-hover/card:opacity-100 absolute top-1 right-1">
// <Menu as="div" className="relative">
// <Menu.Button
// as="button"
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
// >
// <EllipsisHorizontalIcon className="h-4 w-4" />
// </Menu.Button>
// <Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
// <Menu.Item>
// <div className="hover:bg-gray-100 border-b last:border-0">
// <button
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// type="button"
// onClick={() => removeIssueFromCycle(issue.cycle, issue.id)}
// >
// Remove from cycle
// </button>
// </div>
// </Menu.Item>
// <Menu.Item>
// <div className="hover:bg-gray-100 border-b last:border-0">
// <button
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// type="button"
// onClick={() =>
// openCreateIssueModal(cycle.id, childIssue, "delete")
// }
// >
// Delete permanently
// </button>
// </div>
// </Menu.Item>
// </Menu.Items>
// </Menu>
// </div>
// <Link
// href={`/projects/${childIssue.project}/issues/${childIssue.id}`}
// >
// <a>
// {properties.key && (
// <div className="text-xs font-medium text-gray-500 mb-2">
// {activeProject?.identifier}-{childIssue.sequence_id}
// </div>
// )}
// <h5
// className="group-hover:text-theme text-sm break-all mb-3"
// style={{ lineClamp: 3, WebkitLineClamp: 3 }}
// >
// {childIssue.name}
// </h5>
// </a>
// </Link>
// <div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
// {properties.priority && (
// <div
// className={`group flex-shrink-0 flex items-center gap-1 rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
// childIssue.priority === "urgent"
// ? "bg-red-100 text-red-600"
// : childIssue.priority === "high"
// ? "bg-orange-100 text-orange-500"
// : childIssue.priority === "medium"
// ? "bg-yellow-100 text-yellow-500"
// : childIssue.priority === "low"
// ? "bg-green-100 text-green-500"
// : "bg-gray-100"
// }`}
// >
// {/* {getPriorityIcon(childIssue.priority ?? "")} */}
// {childIssue.priority ?? "None"}
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1 text-gray-900">Priority</h5>
// <div
// className={`capitalize ${
// childIssue.priority === "urgent"
// ? "text-red-600"
// : childIssue.priority === "high"
// ? "text-orange-500"
// : childIssue.priority === "medium"
// ? "text-yellow-500"
// : childIssue.priority === "low"
// ? "text-green-500"
// : ""
// }`}
// >
// {childIssue.priority ?? "None"}
// </div>
// </div>
// </div>
// )}
// {properties.state && (
// <div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
// <span
// className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
// style={{ backgroundColor: childIssue.state_detail.color }}
// ></span>
// {addSpaceIfCamelCase(childIssue.state_detail.name)}
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1">State</h5>
// <div>{childIssue.state_detail.name}</div>
// </div>
// </div>
// )}
// {properties.start_date && (
// <div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
// <CalendarDaysIcon className="h-4 w-4" />
// {childIssue.start_date
// ? renderShortNumericDateFormat(childIssue.start_date)
// : "N/A"}
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1">Started at</h5>
// <div>
// {renderShortNumericDateFormat(childIssue.start_date ?? "")}
// </div>
// </div>
// </div>
// )}
// {properties.target_date && (
// <div
// className={`group flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
// childIssue.target_date === null
// ? ""
// : childIssue.target_date < new Date().toISOString()
// ? "text-red-600"
// : findHowManyDaysLeft(childIssue.target_date) <= 3 &&
// "text-orange-400"
// }`}
// >
// <CalendarDaysIcon className="h-4 w-4" />
// {childIssue.target_date
// ? renderShortNumericDateFormat(childIssue.target_date)
// : "N/A"}
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1 text-gray-900">Target date</h5>
// <div>
// {renderShortNumericDateFormat(childIssue.target_date ?? "")}
// </div>
// <div>
// {childIssue.target_date &&
// (childIssue.target_date < new Date().toISOString()
// ? `Target date has passed by ${findHowManyDaysLeft(
// childIssue.target_date
// )} days`
// : findHowManyDaysLeft(childIssue.target_date) <= 3
// ? `Target date is in ${findHowManyDaysLeft(
// childIssue.target_date
// )} days`
// : "Target date")}
// </div>
// </div>
// </div>
// )}
// {properties.assignee && (
// <div className="group flex items-center gap-1 text-xs">
// {childIssue.assignee_details?.length > 0 ? (
// childIssue.assignee_details?.map((assignee, index: number) => (
// <div
// key={index}
// className={`relative z-[1] h-5 w-5 rounded-full ${
// index !== 0 ? "-ml-2.5" : ""
// }`}
// >
// {assignee.avatar && assignee.avatar !== "" ? (
// <div className="h-5 w-5 border-2 bg-white border-white rounded-full">
// <Image
// src={assignee.avatar}
// height="100%"
// width="100%"
// className="rounded-full"
// alt={assignee.name}
// />
// </div>
// ) : (
// <div
// className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
// >
// {assignee.first_name.charAt(0)}
// </div>
// )}
// </div>
// ))
// ) : (
// <div className="h-5 w-5 border-2 bg-white border-white rounded-full">
// <Image
// src={User}
// height="100%"
// width="100%"
// className="rounded-full"
// alt="No user"
// />
// </div>
// )}
// <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1">Assigned to</h5>
// <div>
// {childIssue.assignee_details?.length > 0
// ? childIssue.assignee_details
// .map((assignee) => assignee.first_name)
// .join(", ")
// : "No one"}
// </div>
// </div>
// </div>
// )}
// </div>
// </div>
// </div>
// ))
// ) : (
// <div className="w-full h-full flex justify-center items-center">
// <Spinner />
// </div>
// )}
// <button
// type="button"
// className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
// onClick={() => openCreateIssueModal(cycle.id)}
// >
// <PlusIcon className="h-3 w-3 mr-1" />
// Create
// </button>
// </div>
// </div>
// </div>
// );
};
export default SingleCycleBoard;

View File

@ -47,7 +47,12 @@ const CycleIssuesListModal: React.FC<Props> = ({
reset();
};
const { handleSubmit, reset, control } = useForm<FormInput>({
const {
handleSubmit,
reset,
control,
formState: { isSubmitting },
} = useForm<FormInput>({
defaultValues: {
issue_ids: [],
},
@ -68,6 +73,7 @@ const CycleIssuesListModal: React.FC<Props> = ({
.bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data)
.then((res) => {
console.log(res);
handleClose();
})
.catch((e) => {
console.log(e);
@ -138,36 +144,39 @@ const CycleIssuesListModal: React.FC<Props> = ({
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
>
{({ selected }) => (
<>
<input type="checkbox" checked={selected} readOnly />
<span
className={`h-1.5 w-1.5 block rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
{issue.name}
</>
)}
</Combobox.Option>
))}
{filteredIssues.map((issue) => {
// if (issue.cycle !== cycleId)
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
>
{({ selected }) => (
<>
<input type="checkbox" checked={selected} readOnly />
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
{issue.name}
</>
)}
</Combobox.Option>
);
})}
</ul>
</li>
)}
@ -191,8 +200,13 @@ const CycleIssuesListModal: React.FC<Props> = ({
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button type="button" size="sm" onClick={handleSubmit(handleAddToCycle)}>
Add to Cycle
<Button
type="button"
size="sm"
onClick={handleSubmit(handleAddToCycle)}
disabled={isSubmitting}
>
{isSubmitting ? "Adding..." : "Add to Cycle"}
</Button>
</div>
</form>

View File

@ -1,314 +0,0 @@
// react
import React, { useState } from "react";
// next
import Link from "next/link";
// swr
import useSWR, { mutate } from "swr";
// headless ui
import { Disclosure, Transition, Menu } from "@headlessui/react";
// services
import cycleServices from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
// components
import CycleIssuesListModal from "./CycleIssuesListModal";
// ui
import { Spinner } from "ui";
// icons
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
// types
import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from "types";
// fetch keys
import { CYCLE_ISSUES } from "constants/fetch-keys";
// constants
import { renderShortNumericDateFormat } from "constants/common";
import issuesServices from "lib/services/issues.service";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd";
const CycleView: React.FC<Props> = ({
cycle,
selectSprint,
workspaceSlug,
projectId,
openIssueModal,
}) => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const { activeWorkspace, activeProject, issues } = useUser();
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(CYCLE_ISSUES(cycle.id), () =>
cycleServices.getCycleIssues(workspaceSlug, projectId, cycle.id)
);
const removeIssueFromCycle = (cycleId: string, bridgeId: string) => {
if (activeWorkspace && activeProject && cycleIssues) {
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId),
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
false
);
issuesServices
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId)
.then((res) => {
console.log(res);
})
.catch((e) => {
console.log(e);
});
}
};
return (
<>
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycle.id}
/>
<Disclosure as="div" defaultOpen>
{({ open }) => (
<div className="bg-white px-4 py-2 rounded-lg space-y-3">
<div className="flex items-center">
<Disclosure.Button className="w-full">
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
width={22}
className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
/>
</span>
<h2 className="font-medium leading-5">{cycle.name}</h2>
<p className="flex gap-2 text-xs text-gray-500">
<span>
{cycle.status === "started"
? cycle.start_date
? `${renderShortNumericDateFormat(cycle.start_date)} - `
: ""
: cycle.status}
</span>
<span>
{cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""}
</span>
</p>
</div>
</Disclosure.Button>
<Menu as="div" className="relative inline-block">
<Menu.Button className="grid place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
<Menu.Items className="absolute origin-top-right right-0 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<Menu.Item>
<button
type="button"
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
onClick={() => selectSprint({ ...cycle, actionType: "edit" })}
>
Edit
</button>
</Menu.Item>
<Menu.Item>
<button
type="button"
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
onClick={() => selectSprint({ ...cycle, actionType: "delete" })}
>
Delete
</button>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<StrictModeDroppable droppableId={cycle.id}>
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{cycleIssues ? (
cycleIssues.length > 0 ? (
cycleIssues.map((issue, index) => (
<Draggable
key={issue.id}
draggableId={`${issue.id},${issue.issue}`} // bridge id, issue id
index={index}
>
{(provided, snapshot) => (
<div
className={`group p-2 hover:bg-gray-100 text-sm rounded flex items-center justify-between ${
snapshot.isDragging
? "bg-gray-100 shadow-lg border border-theme"
: ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className="flex items-center gap-2">
<button
type="button"
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 rotate-90 outline-none`}
{...provided.dragHandleProps}
>
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
</button>
<span
className={`h-1.5 w-1.5 block rounded-full`}
style={{
backgroundColor: issue.issue_details.state_detail.color,
}}
/>
<Link
href={`/projects/${projectId}/issues/${issue.issue_details.id}`}
>
<a className="flex items-center gap-2">
<span className="text-xs text-gray-500">
{activeProject?.identifier}-
{issue.issue_details.sequence_id}
</span>
{issue.issue_details.name}
{/* {cycle.id} */}
</a>
</Link>
</div>
<div className="flex items-center gap-2">
<span
className="text-black rounded-md px-2 py-0.5 text-sm"
style={{
backgroundColor: `${issue.issue_details.state_detail?.color}20`,
border: `2px solid ${issue.issue_details.state_detail?.color}`,
}}
>
{issue.issue_details.state_detail?.name}
</span>
<Menu as="div" className="relative">
<Menu.Button
as="button"
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none`}
>
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<Menu.Item>
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
onClick={() =>
openIssueModal(cycle.id, issue.issue_details, "edit")
}
>
Edit
</button>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
onClick={() =>
removeIssueFromCycle(issue.cycle, issue.id)
}
>
Remove from cycle
</button>
</div>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
onClick={() =>
openIssueModal(
cycle.id,
issue.issue_details,
"delete"
)
}
>
Delete permanently
</button>
</div>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
</div>
)}
</Draggable>
))
) : (
<p className="text-sm text-gray-500">This cycle has no issue.</p>
)
) : (
<div className="w-full h-full flex items-center justify-center">
<Spinner />
</div>
)}
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
</Disclosure.Panel>
</Transition>
<Menu as="div" className="relative inline-block">
<Menu.Button className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
<PlusIcon className="h-3 w-3" />
Add issue
</Menu.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<div className="p-1">
<Menu.Item as="div">
{(active) => (
<button
type="button"
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
onClick={() => openIssueModal(cycle.id)}
>
Create new
</button>
)}
</Menu.Item>
<Menu.Item as="div">
{(active) => (
<button
type="button"
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
onClick={() => setCycleIssuesListModal(true)}
>
Add an existing issue
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
)}
</Disclosure>
</>
);
};
export default CycleView;

View File

@ -0,0 +1,714 @@
// react
import React from "react";
// next
import Link from "next/link";
// swr
import useSWR from "swr";
// headless ui
import { Disclosure, Transition, Menu } from "@headlessui/react";
// services
import cycleServices from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Spinner } from "ui";
// icons
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, SelectSprintType } from "types";
// fetch keys
import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants
import {
addSpaceIfCamelCase,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
import workspaceService from "lib/services/workspace.service";
type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
properties: Properties;
selectedGroup: NestedKeyOf<IIssue> | null;
openCreateIssueModal: (
sprintId: string,
issue?: IIssue,
actionType?: "create" | "edit" | "delete"
) => void;
openIssuesListModal: (cycleId: string) => void;
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
};
const CyclesListView: React.FC<Props> = ({
groupedByIssues,
selectedGroup,
openCreateIssueModal,
openIssuesListModal,
properties,
removeIssueFromCycle,
}) => {
const { activeWorkspace, activeProject } = useUser();
const { data: people } = useSWR<IWorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
);
return (
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => (
<Disclosure key={singleGroup} as="div" defaultOpen>
{({ open }) => (
<div className="bg-white rounded-lg">
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium leading-5 capitalize">
{singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority"
: addSpaceIfCamelCase(singleGroup)}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-gray-500 text-sm">
{groupedByIssues[singleGroup as keyof IIssue].length}
</p>
</div>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="divide-y-2">
{groupedByIssues[singleGroup] ? (
groupedByIssues[singleGroup].length > 0 ? (
groupedByIssues[singleGroup].map((issue: IIssue) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find(
(p) => p.member.id === assignee
)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
});
return (
<div
key={issue.id}
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
>
<div className="flex items-center gap-2">
<span
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
)}
<span className="">{issue.name}</span>
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
<h5 className="font-medium mb-1">Name</h5>
<div>{issue.name}</div>
</div>
</a>
</Link>
</div>
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
{properties.priority && (
<div
className={`group relative flex-shrink-0 flex items-center gap-1 text-xs rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
issue.priority === "urgent"
? "bg-red-100 text-red-600"
: issue.priority === "high"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{/* {getPriorityIcon(issue.priority ?? "")} */}
{issue.priority ?? "None"}
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
<div
className={`capitalize ${
issue.priority === "urgent"
? "text-red-600"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"
? "text-yellow-500"
: issue.priority === "low"
? "text-green-500"
: ""
}`}
>
{issue.priority ?? "None"}
</div>
</div>
</div>
)}
{properties.state && (
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: issue?.state_detail?.color,
}}
></span>
{addSpaceIfCamelCase(issue?.state_detail.name)}
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">State</h5>
<div>{issue?.state_detail.name}</div>
</div>
</div>
)}
{properties.start_date && (
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<CalendarDaysIcon className="h-4 w-4" />
{issue.start_date
? renderShortNumericDateFormat(issue.start_date)
: "N/A"}
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">Started at</h5>
<div>
{renderShortNumericDateFormat(issue.start_date ?? "")}
</div>
</div>
</div>
)}
{properties.target_date && (
<div
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 &&
"text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{issue.target_date
? renderShortNumericDateFormat(issue.target_date)
: "N/A"}
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">
Target date
</h5>
<div>
{renderShortNumericDateFormat(issue.target_date ?? "")}
</div>
<div>
{issue.target_date &&
(issue.target_date < new Date().toISOString()
? `Target date has passed by ${findHowManyDaysLeft(
issue.target_date
)} days`
: findHowManyDaysLeft(issue.target_date) <= 3
? `Target date is in ${findHowManyDaysLeft(
issue.target_date
)} days`
: "Target date")}
</div>
</div>
</div>
)}
<Menu as="div" className="relative">
<Menu.Button
as="button"
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
>
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<Menu.Item>
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
// onClick={() =>
// openCreateIssueModal(cycle.id, issue, "edit")
// }
>
Edit
</button>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
// onClick={() =>
// removeIssueFromCycle(issue.cycle, issue.id)
// }
>
Remove from cycle
</button>
</div>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
// onClick={() =>
// openCreateIssueModal(cycle.id, issue, "delete")
// }
>
Delete permanently
</button>
</div>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
</div>
);
})
) : (
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
)
) : (
<div className="h-full w-full flex items-center justify-center">
<Spinner />
</div>
)}
</div>
</Disclosure.Panel>
</Transition>
<div className="p-3">
<button
type="button"
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
// onClick={() => {
// setIsCreateIssuesModalOpen(true);
// if (selectedGroup !== null) {
// const stateId =
// selectedGroup === "state_detail.name"
// ? states?.find((s) => s.name === singleGroup)?.id ?? null
// : null;
// setPreloadedData({
// state: stateId !== null ? stateId : undefined,
// [selectedGroup]: singleGroup,
// actionType: "createIssue",
// });
// }
// }}
>
<PlusIcon className="h-3 w-3" />
Add issue
</button>
</div>
</div>
)}
</Disclosure>
))}
</div>
);
// return (
// <>
// <Disclosure as="div" defaultOpen>
// {({ open }) => (
// <div className="bg-white rounded-lg">
// <div className="flex justify-between items-center bg-gray-100 px-4 py-3 rounded-t-lg">
// <div className="flex items-center gap-2">
// <Disclosure.Button>
// <ChevronDownIcon
// className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
// />
// </Disclosure.Button>
// <Link href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
// <a className="flex items-center gap-2">
// <h2 className="font-medium leading-5">{cycle.name}</h2>
// <p className="flex gap-2 text-xs text-gray-500">
// <span>
// {cycle.status === "started"
// ? cycle.start_date
// ? `${renderShortNumericDateFormat(cycle.start_date)} - `
// : ""
// : cycle.status}
// </span>
// <span>
// {cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""}
// </span>
// </p>
// </a>
// </Link>
// <p className="text-gray-500 text-sm ml-0.5">{cycleIssues?.length}</p>
// </div>
// <Menu as="div" className="relative inline-block">
// <Menu.Button
// as="button"
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none`}
// >
// <EllipsisHorizontalIcon className="h-4 w-4" />
// </Menu.Button>
// <Menu.Items className="absolute origin-top-right right-0 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
// <Menu.Item>
// <button
// type="button"
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// onClick={() => selectSprint({ ...cycle, actionType: "edit" })}
// >
// Edit
// </button>
// </Menu.Item>
// <Menu.Item>
// <button
// type="button"
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// onClick={() => selectSprint({ ...cycle, actionType: "delete" })}
// >
// Delete
// </button>
// </Menu.Item>
// </Menu.Items>
// </Menu>
// </div>
// <Transition
// show={open}
// enter="transition duration-100 ease-out"
// enterFrom="transform opacity-0"
// enterTo="transform opacity-100"
// leave="transition duration-75 ease-out"
// leaveFrom="transform opacity-100"
// leaveTo="transform opacity-0"
// >
// <Disclosure.Panel>
// <StrictModeDroppable droppableId={cycle.id}>
// {(provided) => (
// <div
// ref={provided.innerRef}
// {...provided.droppableProps}
// className="divide-y-2"
// >
// {cycleIssues ? (
// cycleIssues.length > 0 ? (
// cycleIssues.map((issue, index) => (
// <Draggable
// key={issue.id}
// draggableId={`${issue.id},${issue.issue}`} // bridge id, issue id
// index={index}
// >
// {(provided, snapshot) => (
// <div
// className={`px-2 py-3 text-sm rounded flex justify-between items-center gap-2 ${
// snapshot.isDragging
// ? "bg-gray-100 shadow-lg border border-theme"
// : ""
// }`}
// ref={provided.innerRef}
// {...provided.draggableProps}
// >
// <div className="flex items-center gap-2">
// <button
// type="button"
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 rotate-90 outline-none`}
// {...provided.dragHandleProps}
// >
// <EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
// <EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
// </button>
// <span
// className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
// style={{
// backgroundColor: issue?.state_detail?.color,
// }}
// />
// <Link
// href={`/projects/${projectId}/issues/${issue.id}`}
// >
// <a className="flex items-center gap-2">
// {properties.key && (
// <span className="flex-shrink-0 text-xs text-gray-500">
// {activeProject?.identifier}-
// {issue.sequence_id}
// </span>
// )}
// <span>{issue.name}</span>
// </a>
// </Link>
// </div>
// <div className="flex items-center gap-2">
// {properties.priority && (
// <div
// className={`group relative flex-shrink-0 flex items-center gap-1 text-xs rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
// issue.priority === "urgent"
// ? "bg-red-100 text-red-600"
// : issue.priority === "high"
// ? "bg-orange-100 text-orange-500"
// : issue.priority === "medium"
// ? "bg-yellow-100 text-yellow-500"
// : issue.priority === "low"
// ? "bg-green-100 text-green-500"
// : "bg-gray-100"
// }`}
// >
// {/* {getPriorityIcon(issue.priority ?? "")} */}
// {issue.priority ?? "None"}
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1 text-gray-900">
// Priority
// </h5>
// <div
// className={`capitalize ${
// issue.priority === "urgent"
// ? "text-red-600"
// : issue.priority === "high"
// ? "text-orange-500"
// : issue.priority === "medium"
// ? "text-yellow-500"
// : issue.priority === "low"
// ? "text-green-500"
// : ""
// }`}
// >
// {issue.priority ?? "None"}
// </div>
// </div>
// </div>
// )}
// {properties.state && (
// <div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
// <span
// className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
// style={{
// backgroundColor:
// issue?.state_detail?.color,
// }}
// ></span>
// {addSpaceIfCamelCase(
// issue?.state_detail.name
// )}
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1">State</h5>
// <div>{issue?.state_detail.name}</div>
// </div>
// </div>
// )}
// {properties.start_date && (
// <div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
// <CalendarDaysIcon className="h-4 w-4" />
// {issue.start_date
// ? renderShortNumericDateFormat(
// issue.start_date
// )
// : "N/A"}
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1">Started at</h5>
// <div>
// {renderShortNumericDateFormat(
// issue.start_date ?? ""
// )}
// </div>
// </div>
// </div>
// )}
// {properties.target_date && (
// <div
// className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
// issue.target_date === null
// ? ""
// : issue.target_date <
// new Date().toISOString()
// ? "text-red-600"
// : findHowManyDaysLeft(
// issue.target_date
// ) <= 3 && "text-orange-400"
// }`}
// >
// <CalendarDaysIcon className="h-4 w-4" />
// {issue.target_date
// ? renderShortNumericDateFormat(
// issue.target_date
// )
// : "N/A"}
// <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
// <h5 className="font-medium mb-1 text-gray-900">
// Target date
// </h5>
// <div>
// {renderShortNumericDateFormat(
// issue.target_date ?? ""
// )}
// </div>
// <div>
// {issue.target_date &&
// (issue.target_date <
// new Date().toISOString()
// ? `Target date has passed by ${findHowManyDaysLeft(
// issue.target_date
// )} days`
// : findHowManyDaysLeft(
// issue.target_date
// ) <= 3
// ? `Target date is in ${findHowManyDaysLeft(
// issue.target_date
// )} days`
// : "Target date")}
// </div>
// </div>
// </div>
// )}
// <Menu as="div" className="relative">
// <Menu.Button
// as="button"
// className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
// >
// <EllipsisHorizontalIcon className="h-4 w-4" />
// </Menu.Button>
// <Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
// <Menu.Item>
// <button
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// type="button"
// onClick={() =>
// openCreateIssueModal(
// cycle.id,
// issue,
// "edit"
// )
// }
// >
// Edit
// </button>
// </Menu.Item>
// <Menu.Item>
// <div className="hover:bg-gray-100 border-b last:border-0">
// <button
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// type="button"
// onClick={() =>
// removeIssueFromCycle(issue.cycle, issue.id)
// }
// >
// Remove from cycle
// </button>
// </div>
// </Menu.Item>
// <Menu.Item>
// <div className="hover:bg-gray-100 border-b last:border-0">
// <button
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// type="button"
// onClick={() =>
// openCreateIssueModal(
// cycle.id,
// issue,
// "delete"
// )
// }
// >
// Delete permanently
// </button>
// </div>
// </Menu.Item>
// </Menu.Items>
// </Menu>
// </div>
// </div>
// )}
// </Draggable>
// ))
// ) : (
// <p className="text-sm px-4 py-3 text-gray-500">
// This cycle has no issue.
// </p>
// )
// ) : (
// <div className="w-full h-full flex items-center justify-center">
// <Spinner />
// </div>
// )}
// {provided.placeholder}
// </div>
// )}
// </StrictModeDroppable>
// </Disclosure.Panel>
// </Transition>
// <div className="p-3">
// <Menu as="div" className="relative inline-block">
// <Menu.Button className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
// <PlusIcon className="h-3 w-3" />
// Add issue
// </Menu.Button>
// <Transition
// as={React.Fragment}
// enter="transition ease-out duration-100"
// enterFrom="transform opacity-0 scale-95"
// enterTo="transform opacity-100 scale-100"
// leave="transition ease-in duration-75"
// leaveFrom="transform opacity-100 scale-100"
// leaveTo="transform opacity-0 scale-95"
// >
// <Menu.Items className="absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
// <div className="p-1">
// <Menu.Item as="div">
// {(active) => (
// <button
// type="button"
// className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
// onClick={() => openCreateIssueModal(cycle.id)}
// >
// Create new
// </button>
// )}
// </Menu.Item>
// <Menu.Item as="div">
// {(active) => (
// <button
// type="button"
// className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
// onClick={() => openIssuesListModal(cycle.id)}
// >
// Add an existing issue
// </button>
// )}
// </Menu.Item>
// </div>
// </Menu.Items>
// </Transition>
// </Menu>
// </div>
// </div>
// )}
// </Disclosure>
// </>
// );
};
export default CyclesListView;

View File

@ -1,327 +0,0 @@
import React, { useState } from "react";
// Next imports
import Link from "next/link";
// React beautiful dnd
import { Draggable } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// common
import {
addSpaceIfCamelCase,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
// types
import { IIssue, Properties, NestedKeyOf } from "types";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
CalendarDaysIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
import Image from "next/image";
import { getPriorityIcon } from "constants/global";
type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
groupedByIssues: {
[key: string]: IIssue[];
};
index: number;
setIsIssueOpen: React.Dispatch<React.SetStateAction<boolean>>;
properties: Properties;
setPreloadedData: React.Dispatch<
React.SetStateAction<
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| undefined
>
>;
bgColor?: string;
stateId: string | null;
createdBy: string | null;
};
const SingleBoard: React.FC<Props> = ({
selectedGroup,
groupTitle,
groupedByIssues,
index,
setIsIssueOpen,
properties,
setPreloadedData,
bgColor = "#0f2b16",
stateId,
createdBy,
}) => {
// Collapse/Expand
const [show, setState] = useState<any>(true);
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
return (
<Draggable draggableId={groupTitle} index={index}>
{(provided, snapshot) => (
<div
className={`rounded flex-shrink-0 h-full ${
snapshot.isDragging ? "border-theme shadow-lg" : ""
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
<div
className={`flex justify-between p-3 pb-0 ${
!show ? "flex-col bg-gray-50 rounded-md border" : ""
}`}
>
<div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
<button
type="button"
{...provided.dragHandleProps}
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
!show ? "" : "rotate-90"
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
>
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
</button>
<div
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
}`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
>
<span
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
style={{
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
}}
/>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !show ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="text-gray-500 text-sm ml-0.5">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
<div className={`flex items-center ${!show ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
onClick={() => {
setState(!show);
}}
>
{show ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</button>
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
onClick={() => {
setIsIssueOpen(true);
if (selectedGroup !== null)
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}}
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
</div>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!show ? "hidden" : "block"}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{groupedByIssues[groupTitle].map((childIssue, index: number) => (
<Draggable key={childIssue.id} draggableId={childIssue.id} index={index}>
{(provided, snapshot) => (
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
<a
className={`group block border rounded bg-white shadow-sm ${
snapshot.isDragging ? "border-indigo-600 shadow-lg bg-indigo-50" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className="p-2 select-none" {...provided.dragHandleProps}>
{properties.key && (
<div className="text-xs font-medium text-gray-500 mb-2">
{childIssue.project_detail?.identifier}-{childIssue.sequence_id}
</div>
)}
<h5 className="group-hover:text-theme text-sm break-all mb-3">
{childIssue.name}
</h5>
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
{properties.priority && (
<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 ${
childIssue.priority === "high"
? "bg-red-100 text-red-600"
: childIssue.priority === "medium"
? "bg-orange-100 text-orange-500"
: childIssue.priority === "low"
? "bg-green-100 text-green-500"
: "hidden"
}`}
>
{/* {getPriorityIcon(childIssue.priority ?? "")} */}
{childIssue.priority}
</div>
)}
{properties.state && (
<div className="flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: childIssue.state_detail.color }}
></span>
{addSpaceIfCamelCase(childIssue.state_detail.name)}
</div>
)}
{properties.start_date && (
<div className="flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<CalendarDaysIcon className="h-4 w-4" />
{childIssue.start_date
? renderShortNumericDateFormat(childIssue.start_date)
: "N/A"}
</div>
)}
{properties.target_date && (
<div
className={`flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
childIssue.target_date === null
? ""
: childIssue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(childIssue.target_date) <= 3 &&
"text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{childIssue.target_date
? renderShortNumericDateFormat(childIssue.target_date)
: "N/A"}
{childIssue.target_date && (
<span className="absolute -top-full mb-2 left-4 border transition-opacity opacity-0 group-hover:opacity-100 bg-white rounded px-2 py-1">
{childIssue.target_date < new Date().toISOString()
? `Target date has passed by ${findHowManyDaysLeft(
childIssue.target_date
)} days`
: findHowManyDaysLeft(childIssue.target_date) <= 3
? `Target date is in ${findHowManyDaysLeft(
childIssue.target_date
)} days`
: "Target date"}
</span>
)}
</div>
)}
{properties.assignee && (
<div className="justify-end w-full flex items-center gap-1 text-xs">
{childIssue?.assignee_details?.length > 0 ? (
childIssue?.assignee_details?.map(
(assignee, index: number) => (
<div
key={index}
className={`relative z-[1] h-5 w-5 rounded-full ${
index !== 0 ? "-ml-2.5" : ""
}`}
>
{assignee.avatar && assignee.avatar !== "" ? (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<Image
src={assignee.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={assignee.name}
/>
</div>
) : (
<div
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
>
{assignee.first_name.charAt(0)}
</div>
)}
</div>
)
)
) : (
<span>No assignee.</span>
)}
</div>
)}
</div>
</div>
</a>
</Link>
)}
</Draggable>
))}
{provided.placeholder}
<button
type="button"
className="flex items-center text-xs font-medium hover:bg-gray-200 p-2 rounded duration-300 outline-none"
onClick={() => {
setIsIssueOpen(true);
if (selectedGroup !== null) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}
}}
>
<PlusIcon className="h-3 w-3 mr-1" />
Create
</button>
</div>
)}
</StrictModeDroppable>
</div>
</div>
)}
</Draggable>
);
};
export default SingleBoard;

View File

@ -14,15 +14,14 @@ import useUser from "lib/hooks/useUser";
// fetching keys
import { STATE_LIST } from "constants/fetch-keys";
// components
import SingleBoard from "components/project/issues/BoardView/SingleBoard";
import SingleBoard from "components/project/issues/BoardView/single-board";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
// ui
import { Spinner } from "ui";
// types
import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types";
import ConfirmIssueDeletion from "../ConfirmIssueDeletion";
import { TrashIcon } from "@heroicons/react/24/outline";
import ConfirmIssueDeletion from "../confirm-issue-deletion";
type Props = {
properties: Properties;
@ -31,9 +30,18 @@ type Props = {
[key: string]: IIssue[];
};
members: IProjectMember[] | undefined;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
};
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues, members }) => {
const BoardView: React.FC<Props> = ({
properties,
selectedGroup,
groupedByIssues,
members,
handleDeleteIssue,
partialUpdateIssue,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isIssueOpen, setIsIssueOpen] = useState(false);
@ -217,6 +225,8 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
? states?.find((s) => s.name === singleGroup)?.color
: undefined
}
handleDeleteIssue={handleDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
/>
))}
</div>

View File

@ -0,0 +1,609 @@
// react
import React, { useState } from "react";
// next
import Link from "next/link";
import Image from "next/image";
// swr
import useSWR from "swr";
// react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import workspaceService from "lib/services/workspace.service";
// hooks
import useUser from "lib/hooks/useUser";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
CalendarDaysIcon,
EllipsisHorizontalIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import User from "public/user.png";
// common
import { PRIORITIES } from "constants/";
import {
addSpaceIfCamelCase,
classNames,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { getPriorityIcon } from "constants/global";
// types
import { IIssue, Properties, NestedKeyOf, IWorkspaceMember } from "types";
type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
groupedByIssues: {
[key: string]: IIssue[];
};
index: number;
setIsIssueOpen: React.Dispatch<React.SetStateAction<boolean>>;
properties: Properties;
setPreloadedData: React.Dispatch<
React.SetStateAction<
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| undefined
>
>;
bgColor?: string;
stateId: string | null;
createdBy: string | null;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, childIssueId: string) => void;
};
const SingleBoard: React.FC<Props> = ({
selectedGroup,
groupTitle,
groupedByIssues,
index,
setIsIssueOpen,
properties,
setPreloadedData,
bgColor = "#0f2b16",
stateId,
createdBy,
handleDeleteIssue,
partialUpdateIssue,
}) => {
// Collapse/Expand
const [show, setShow] = useState(true);
const { activeProject, activeWorkspace, states } = useUser();
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
const { data: people } = useSWR<IWorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
);
return (
<Draggable draggableId={groupTitle} index={index}>
{(provided, snapshot) => (
<div
className={`rounded flex-shrink-0 h-full ${
snapshot.isDragging ? "border-theme shadow-lg" : ""
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
<div
className={`flex justify-between p-3 pb-0 ${
!show ? "flex-col bg-gray-50 rounded-md border" : ""
}`}
>
<div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
<button
type="button"
{...provided.dragHandleProps}
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
!show ? "" : "rotate-90"
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
>
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
</button>
<div
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
}`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
>
<span
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
style={{
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
}}
/>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !show ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="text-gray-500 text-sm ml-0.5">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
<div className={`flex items-center ${!show ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
onClick={() => {
setShow(!show);
}}
>
{show ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon 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"
onClick={() => {
setIsIssueOpen(true);
if (selectedGroup !== null)
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}}
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
</div>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!show ? "hidden" : "block"}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
const assignees = [
...(childIssue?.assignees_list ?? []),
...(childIssue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
});
return (
<Draggable key={childIssue.id} draggableId={childIssue.id} index={index}>
{(provided, snapshot) => (
<div
className={`border rounded bg-white shadow-sm ${
snapshot.isDragging ? "border-theme shadow-lg bg-indigo-50" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className="group/card relative p-2 select-none"
{...provided.dragHandleProps}
>
<div className="opacity-0 group-hover/card:opacity-100 absolute top-1 right-1">
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded text-red-500 hover:bg-red-50 duration-300 outline-none"
onClick={() => handleDeleteIssue(childIssue.id)}
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
<Link
href={`/projects/${childIssue.project}/issues/${childIssue.id}`}
>
<a>
{properties.key && (
<div className="text-xs font-medium text-gray-500 mb-2">
{activeProject?.identifier}-{childIssue.sequence_id}
</div>
)}
<h5
className="group-hover:text-theme text-sm break-all mb-3"
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
>
{childIssue.name}
</h5>
</a>
</Link>
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
{properties.priority && (
<Listbox
as="div"
value={childIssue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data }, childIssue.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 ${
childIssue.priority === "urgent"
? "bg-red-100 text-red-600"
: childIssue.priority === "high"
? "bg-orange-100 text-orange-500"
: childIssue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: childIssue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{childIssue.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 ${
childIssue.priority === "urgent"
? "text-red-600"
: childIssue.priority === "high"
? "text-orange-500"
: childIssue.priority === "medium"
? "text-yellow-500"
: childIssue.priority === "low"
? "text-green-500"
: ""
}`}
>
{childIssue.priority ?? "None"}
</div>
</div>
</>
)}
</Listbox>
)}
{properties.state && (
<Listbox
as="div"
value={childIssue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data }, childIssue.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: childIssue.state_detail.color,
}}
></span>
{addSpaceIfCamelCase(childIssue.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>{childIssue.state_detail.name}</div>
</div>
</>
)}
</Listbox>
)}
{properties.start_date && (
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<CalendarDaysIcon className="h-4 w-4" />
{childIssue.start_date
? renderShortNumericDateFormat(childIssue.start_date)
: "N/A"}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">Started at</h5>
<div>
{renderShortNumericDateFormat(childIssue.start_date ?? "")}
</div>
</div>
</div>
)}
{properties.target_date && (
<div
className={`group flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
childIssue.target_date === null
? ""
: childIssue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(childIssue.target_date) <= 3 &&
"text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{childIssue.target_date
? renderShortNumericDateFormat(childIssue.target_date)
: "N/A"}
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">
Target date
</h5>
<div>
{renderShortNumericDateFormat(childIssue.target_date ?? "")}
</div>
<div>
{childIssue.target_date &&
(childIssue.target_date < new Date().toISOString()
? `Target date has passed by ${findHowManyDaysLeft(
childIssue.target_date
)} days`
: findHowManyDaysLeft(childIssue.target_date) <= 3
? `Target date is in ${findHowManyDaysLeft(
childIssue.target_date
)} days`
: "Target date")}
</div>
</div>
</div>
)}
{properties.assignee && (
<Listbox
as="div"
value={childIssue.assignees}
onChange={(data: any) => {
const newData = childIssue.assignees ?? [];
if (newData.includes(data)) {
newData.splice(newData.indexOf(data), 1);
} else {
newData.push(data);
}
partialUpdateIssue(
{ assignees_list: newData },
childIssue.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 left-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 left-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>
{childIssue.assignee_details?.length > 0
? childIssue.assignee_details
.map((assignee) => assignee.first_name)
.join(", ")
: "No one"}
</div>
</div>
</>
)}
</Listbox>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
<button
type="button"
className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
onClick={() => {
setIsIssueOpen(true);
if (selectedGroup !== null) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}
}}
>
<PlusIcon className="h-3 w-3 mr-1" />
Create
</button>
</div>
)}
</StrictModeDroppable>
</div>
</div>
)}
</Draggable>
);
};
export default SingleBoard;

View File

@ -106,12 +106,16 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
className={({ active }) =>
`${
active ? "text-white bg-theme" : "text-gray-900"
} cursor-pointer select-none w-full p-2 rounded-md`
} flex items-center gap-2 cursor-pointer select-none w-full p-2 rounded-md`
}
value={label.id}
>
{({ selected, active }) => (
<>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: label.colour }}
></span>
<span
className={`${
selected || (value ?? []).some((i) => i === label.id)

View File

@ -34,11 +34,14 @@ import SelectAssignee from "./SelectAssignee";
import SelectParent from "./SelectParentIssue";
import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal";
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
// types
import type { IIssue, IssueResponse, CycleIssueResponse } from "types";
import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
ssr: false,
});
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -78,10 +81,6 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
// setIssueDescriptionValue(value);
// };
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
ssr: false,
});
const router = useRouter();
const handleClose = () => {
@ -117,7 +116,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => {
if (!activeWorkspace || !activeProject) return;
await issuesServices
.addIssueToSprint(activeWorkspace.slug, activeProject.id, sprintId, {
.addIssueToCycle(activeWorkspace.slug, activeProject.id, sprintId, {
issue: issueId,
})
.then((res) => {
@ -176,6 +175,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
const payload: Partial<IIssue> = {
...formData,
target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null,
// description: formData.description ? JSON.parse(formData.description) : null,
};
if (!data) {
await issuesServices

View File

@ -4,23 +4,37 @@ import React, { useState } from "react";
import Link from "next/link";
import Image from "next/image";
// swr
import useSWR, { mutate } from "swr";
import useSWR from "swr";
// headless ui
import { Disclosure, Listbox, Menu, Transition } from "@headlessui/react";
// ui
import { Listbox, Transition } from "@headlessui/react";
import { Spinner } from "ui";
// icons
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import {
ChevronDownIcon,
PlusIcon,
CalendarDaysIcon,
EllipsisHorizontalIcon,
} from "@heroicons/react/24/outline";
import User from "public/user.png";
// components
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
// types
import { IIssue, IssueResponse, NestedKeyOf, Properties } from "types";
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
// services
import workspaceService from "lib/services/workspace.service";
// hooks
import useUser from "lib/hooks/useUser";
// fetch keys
import { PRIORITIES } from "constants/";
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// services
import issuesServices from "lib/services/issues.service";
import workspaceService from "lib/services/workspace.service";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants
import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common";
import {
addSpaceIfCamelCase,
classNames,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
// types
type Props = {
@ -29,6 +43,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
setSelectedIssue: any;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
};
const ListView: React.FC<Props> = ({
@ -37,377 +52,515 @@ const ListView: React.FC<Props> = ({
selectedGroup,
setSelectedIssue,
handleDeleteIssue,
partialUpdateIssue,
}) => {
const [isCreateIssuesModalOpen, setIsCreateIssuesModalOpen] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
const { activeWorkspace, activeProject, states } = useUser();
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
if (!activeWorkspace || !activeProject) return;
issuesServices
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, formData)
.then((response) => {
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
(prevData) => ({
...(prevData as IssueResponse),
results:
prevData?.results.map((issue) => (issue.id === response.id ? response : issue)) ?? [],
}),
false
);
})
.catch((error) => {
console.log(error);
});
};
const { data: people } = useSWR(
const { data: people } = useSWR<IWorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
);
return (
<div className="mt-4 flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => (
<div key={singleGroup} className="overflow-x-auto">
<div className="inline-block min-w-full p-0.5 align-middle">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full">
{selectedGroup !== null ? (
<thead className="bg-gray-100">
<tr>
<th
colSpan={14}
scope="col"
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
>
<div className="flex items-center gap-2">
{selectedGroup === "state_detail.name" ? (
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: states?.find((s) => s.name === singleGroup)?.color,
}}
></span>
) : null}
<>
<CreateUpdateIssuesModal
isOpen={isCreateIssuesModalOpen && preloadedData?.actionType === "createIssue"}
setIsOpen={setIsCreateIssuesModalOpen}
prePopulateData={{
...preloadedData,
}}
projectId={activeProject?.id as string}
/>
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => (
<Disclosure key={singleGroup} as="div" defaultOpen>
{({ open }) => (
<div className="bg-white rounded-lg">
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium leading-5 capitalize">
{singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority"
: addSpaceIfCamelCase(singleGroup)}
<span className="ml-2 text-gray-500 font-normal text-sm">
{groupedByIssues[singleGroup as keyof IIssue].length}
</span>
</div>
</th>
</tr>
</thead>
) : (
<thead className="bg-gray-100">
<tr>
<th
colSpan={14}
scope="col"
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
>
ALL ISSUES
<span className="ml-2 text-gray-500 font-normal text-sm">
{groupedByIssues[singleGroup as keyof IIssue].length}
</span>
</th>
</tr>
</thead>
)}
<tbody className="bg-white">
{groupedByIssues[singleGroup].length > 0
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map(
(assignee) => people?.find((p) => p.member.id === assignee)?.member.email
);
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-gray-500 text-sm">
{groupedByIssues[singleGroup as keyof IIssue].length}
</p>
</div>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="divide-y-2">
{groupedByIssues[singleGroup] ? (
groupedByIssues[singleGroup].length > 0 ? (
groupedByIssues[singleGroup].map((issue: IIssue) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find(
(p) => p.member.id === assignee
)?.member;
return (
<tr
key={issue.id}
className={classNames(
index === 0 ? "border-gray-300" : "border-gray-200",
"border-t"
)}
>
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]">
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
<a className="hover:text-theme duration-300">{issue.name}</a>
</Link>
</td>
{Object.keys(properties).map(
(key) =>
properties[key as keyof Properties] && (
<React.Fragment key={key}>
{(key as keyof Properties) === "key" ? (
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
{activeProject?.identifier}-{issue.sequence_id}
</td>
) : (key as keyof Properties) === "priority" ? (
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
<Listbox
as="div"
value={issue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data }, issue.id);
}}
className="flex-shrink-0"
>
{({ open }) => (
<>
<div className="">
<Listbox.Button className="inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-1 px-0.5 text-xs font-medium text-gray-500 hover:bg-gray-100 border">
<span
className={classNames(
issue.priority ? "" : "text-gray-900",
"hidden truncate capitalize sm:block w-16"
)}
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
});
return (
<div
key={issue.id}
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
>
<div className="flex items-center gap-2">
<span
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
)}
<span className="">{issue.name}</span>
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
<h5 className="font-medium mb-1">Name</h5>
<div>{issue.name}</div>
</div>
</a>
</Link>
</div>
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
{properties.priority && (
<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}
>
{issue.priority ?? "None"}
</span>
</Listbox.Button>
{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">
{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>
</>
)}
</Listbox>
</td>
) : (key as keyof Properties) === "assignee" ? (
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
<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="flex-shrink-0"
>
{({ open }) => (
<>
<div>
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border">
{() => {
if (assignees.length > 0)
return (
<>
{assignees.map((assignee, index) => (
<div
key={index}
className={
"hidden truncate sm:block text-left"
}
>
{assignee}
</div>
))}
</>
);
else return <span>None</span>;
}}
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 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 px-3 py-2"
)
}
value={person.member.id}
>
<div
className={`flex items-center gap-x-1 ${
assignees.includes(
person.member.first_name
)
? "font-medium"
: "font-normal"
}`}
>
{person.member.avatar &&
person.member.avatar !== "" ? (
<div className="relative w-4 h-4">
<Image
src={person.member.avatar}
alt="avatar"
className="rounded-full"
layout="fill"
objectFit="cover"
/>
</div>
) : (
<p>
{person.member.first_name.charAt(0)}
</p>
)}
<p>{person.member.first_name}</p>
<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>
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
) : (
<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>
</td>
) : (key as keyof Properties) === "state" ? (
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
<Listbox
as="div"
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data }, issue.id);
}}
className="flex-shrink-0"
>
{({ open }) => (
<>
<div>
<Listbox.Button
className="inline-flex items-center whitespace-nowrap rounded-full px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 border"
style={{
border: `2px solid ${issue.state_detail.color}`,
backgroundColor: `${issue.state_detail.color}20`,
}}
>
<span
className={classNames(
issue.state ? "" : "text-gray-900",
"hidden capitalize sm:block w-16"
)}
>
{addSpaceIfCamelCase(issue.state_detail.name)}
</span>
</Listbox.Button>
</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>
</>
)}
</Listbox>
</td>
) : (key as keyof Properties) === "target_date" ? (
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
{issue.target_date
? renderShortNumericDateFormat(issue.target_date)
: "-"}
</td>
) : (
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize">
{issue[key as keyof IIssue] ??
(issue[key as keyof IIssue] as any)?.name ??
"None"}
</td>
)}
</React.Fragment>
)
)}
<td className="px-3">
<div className="flex justify-end items-center gap-2">
<button
type="button"
className="flex items-center bg-blue-100 text-blue-600 hover:bg-blue-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
onClick={() => {
setSelectedIssue({
...issue,
actionType: "edit",
});
}}
>
<PencilIcon className="h-3 w-3" />
</button>
<button
type="button"
className="flex items-center bg-red-100 text-red-600 hover:bg-red-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
onClick={() => {
handleDeleteIssue(issue.id);
}}
>
<TrashIcon className="h-3 w-3" />
</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>
</td>
</tr>
);
})
: null}
</tbody>
</table>
</div>
</div>
</div>
))}
</div>
);
})
) : (
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
)
) : (
<div className="h-full w-full flex items-center justify-center">
<Spinner />
</div>
)}
</div>
</Disclosure.Panel>
</Transition>
<div className="p-3">
<button
type="button"
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
onClick={() => {
setIsCreateIssuesModalOpen(true);
if (selectedGroup !== null) {
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: singleGroup,
actionType: "createIssue",
});
}
}}
>
<PlusIcon className="h-3 w-3" />
Add issue
</button>
</div>
</div>
)}
</Disclosure>
))}
</div>
</>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
// swr
import { mutate } from "swr";
// headless ui
@ -26,7 +26,7 @@ type Props = {
const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { activeWorkspace } = useUser();
const { activeWorkspace, activeProject } = useUser();
const { setToastAlert } = useToast();
@ -70,7 +70,7 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={onClose}>
<Dialog as="div" className="relative z-20" initialFocus={cancelButtonRef} onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -97,22 +97,24 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
Delete Issue
<div>
<div className="mx-auto h-16 w-16 grid place-items-center rounded-full bg-red-100">
<ExclamationTriangleIcon
className="h-8 w-8 text-red-600"
aria-hidden="true"
/>
</div>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 mt-3"
>
Are you sure you want to delete {`"`}
{activeProject?.identifier}-{data?.sequence_id} - {data?.name}?{`"`}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete issue - {`"`}
<span className="italic">{data?.name}</span>
{`"`} ? All of the data related to the issue will be permanently removed.
This action cannot be undone.
All of the data related to the issue will be permanently removed. This
action cannot be undone.
</p>
</div>
</div>

View File

@ -12,6 +12,8 @@ import workspaceService from "lib/services/workspace.service";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// components
import IssuesListModal from "components/project/issues/IssuesListModal";
// fetching keys
import {
PROJECT_ISSUES_LIST,
@ -37,18 +39,22 @@ import {
LinkIcon,
ArrowPathIcon,
CalendarDaysIcon,
TrashIcon,
PlusIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
// types
import type { Control } from "react-hook-form";
import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types";
import { TwitterPicker } from "react-color";
import IssuesListModal from "components/project/issues/IssuesListModal";
import { positionEditorElement } from "components/lexical/helpers/editor";
type Props = {
control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void;
issueDetail: IIssue | undefined;
watch: UseFormWatch<IIssue>;
setDeleteIssueModal: React.Dispatch<React.SetStateAction<boolean>>;
};
const defaultValues: Partial<IIssueLabels> = {
@ -58,13 +64,15 @@ const defaultValues: Partial<IIssueLabels> = {
const IssueDetailSidebar: React.FC<Props> = ({
control,
watch: watchIssue,
submitChanges,
issueDetail,
watch: watchIssue,
setDeleteIssueModal,
}) => {
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
const [createLabelForm, setCreateLabelForm] = useState(false);
const { activeWorkspace, activeProject, cycles, issues } = useUser();
@ -117,7 +125,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
name: NestedKeyOf<IIssue>;
canSelectMultipleOptions: boolean;
icon: (props: any) => JSX.Element;
options?: Array<{ label: string; value: any }>;
options?: Array<{ label: string; value: any; color?: string }>;
modal: boolean;
issuesList?: Array<IIssue>;
isOpen?: boolean;
@ -133,6 +141,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
options: states?.map((state) => ({
label: state.name,
value: state.id,
color: state.color,
})),
modal: false,
},
@ -221,364 +230,411 @@ const IssueDetailSidebar: React.FC<Props> = ({
const handleCycleChange = (cycleId: string) => {
if (activeWorkspace && activeProject && issueDetail)
issuesServices.addIssueToSprint(activeWorkspace.slug, activeProject.id, cycleId, {
issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, {
issue: issueDetail.id,
});
};
return (
<div className="h-full w-full divide-y-2 divide-gray-100">
<div className="flex justify-between items-center pb-3">
<h4 className="text-sm font-medium">
{activeProject?.identifier}-{issueDetail?.sequence_id}
</h4>
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
)
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<LinkIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() =>
copyTextToClipboard(`${issueDetail?.id}`)
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
</button>
<>
<div className="h-full w-full divide-y-2 divide-gray-100">
<div className="flex justify-between items-center pb-3">
<h4 className="text-sm font-medium">
{activeProject?.identifier}-{issueDetail?.sequence_id}
</h4>
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
)
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<LinkIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() =>
copyTextToClipboard(`${issueDetail?.id}`)
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-2 hover:bg-red-50 text-red-500 border border-red-500 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() => setDeleteIssueModal(true)}
>
<TrashIcon className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
<div className="divide-y-2 divide-gray-100">
{sidebarSections.map((section, index) => (
<div key={index} className="py-1">
{section.map((item) => (
<div key={item.label} className="flex items-center py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<item.icon className="flex-shrink-0 h-4 w-4" />
<p>{item.label}</p>
</div>
<div className="sm:basis-1/2">
{item.name === "target_date" ? (
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<input
type="date"
value={value ?? ""}
onChange={(e: any) => {
submitChanges({ target_date: e.target.value });
onChange(e.target.value);
}}
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
/>
)}
/>
) : item.modal ? (
<Controller
control={control}
name={item.name as keyof IIssue}
render={({ field: { value, onChange } }) => (
<>
<IssuesListModal
isOpen={Boolean(item?.isOpen)}
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
onChange={(val) => {
console.log(val);
// submitChanges({ [item.name]: val });
onChange(val);
<div className="divide-y-2 divide-gray-100">
{sidebarSections.map((section, index) => (
<div key={index} className="py-1">
{section.map((item) => (
<div key={item.label} className="flex items-center py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<item.icon className="flex-shrink-0 h-4 w-4" />
<p>{item.label}</p>
</div>
<div className="sm:basis-1/2">
{item.name === "target_date" ? (
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<input
type="date"
value={value ?? ""}
onChange={(e: any) => {
submitChanges({ target_date: e.target.value });
onChange(e.target.value);
}}
issues={item?.issuesList ?? []}
title={`Select ${item.label}`}
multiple={item.canSelectMultipleOptions}
value={value}
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
/>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
onClick={() => item.setIsOpen && item.setIsOpen(true)}
>
{watchIssue(`${item.name as keyof IIssue}`) &&
watchIssue(`${item.name as keyof IIssue}`) !== ""
? `${activeProject?.identifier}-
)}
/>
) : item.modal ? (
<Controller
control={control}
name={item.name as keyof IIssue}
render={({ field: { value, onChange } }) => (
<>
<IssuesListModal
isOpen={Boolean(item?.isOpen)}
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
onChange={(val) => {
console.log(val);
// submitChanges({ [item.name]: val });
onChange(val);
}}
issues={item?.issuesList ?? []}
title={`Select ${item.label}`}
multiple={item.canSelectMultipleOptions}
value={value}
/>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
onClick={() => item.setIsOpen && item.setIsOpen(true)}
>
{watchIssue(`${item.name as keyof IIssue}`) &&
watchIssue(`${item.name as keyof IIssue}`) !== ""
? `${activeProject?.identifier}-
${
issues?.results.find(
(i) => i.id === watchIssue(`${item.name as keyof IIssue}`)
)?.sequence_id
}`
: `Select ${item.label}`}
</button>
</>
)}
/>
) : (
<Controller
control={control}
name={item.name as keyof IIssue}
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
multiple={item.canSelectMultipleOptions}
onChange={(value: any) => {
if (item.name === "cycle") handleCycleChange(value);
else submitChanges({ [item.name]: value });
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden truncate sm:block text-left",
item.label === "Priority" ? "capitalize" : ""
)}
>
{value
? Array.isArray(value)
? value
.map(
(i: any) =>
item.options?.find((option) => option.value === i)
?.label
)
.join(", ") || item.label
: item.options?.find((option) => option.value === value)
?.label
: "None"}
</span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1">
{item.options ? (
item.options.length > 0 ? (
item.options.map((option) => (
<Listbox.Option
key={option.value}
className={({ active, selected }) =>
`${
active || selected
? "text-white bg-theme"
: "text-gray-900"
} ${
item.label === "Priority" && "capitalize"
} cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={option.value}
>
{option.label}
</Listbox.Option>
))
) : (
<div className="text-center">No {item.label}s found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
)}
/>
)}
</div>
</div>
))}
</div>
))}
</div>
<div className="space-y-2 pt-3">
<h5 className="text-xs font-medium">Add new label</h5>
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(onSubmit)}>
<div>
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`bg-white flex items-center gap-1 rounded-md p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
>
{watch("colour") && watch("colour") !== "" && (
<span
className="w-6 h-6 rounded"
style={{
backgroundColor: watch("colour") ?? "green",
}}
></span>
)}
<ChevronDownIcon className="h-4 w-4" />
</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 z-10 transform right-0 mt-1 px-2 max-w-xs sm:px-0">
<Controller
name="colour"
control={controlLabel}
render={({ field: { value, onChange } }) => (
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
: `Select ${item.label}`}
</button>
</>
)}
/>
</Popover.Panel>
</Transition>
) : (
<Controller
control={control}
name={item.name as keyof IIssue}
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
multiple={item.canSelectMultipleOptions}
onChange={(value: any) => {
if (item.name === "cycle") handleCycleChange(value);
else submitChanges({ [item.name]: value });
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden truncate sm:block text-left",
item.label === "Priority" ? "capitalize" : ""
)}
>
{value
? Array.isArray(value)
? value
.map(
(i: any) =>
item.options?.find((option) => option.value === i)
?.label
)
.join(", ") || item.label
: item.options?.find((option) => option.value === value)
?.label
: "None"}
</span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1">
{item.options ? (
item.options.length > 0 ? (
item.options.map((option) => (
<Listbox.Option
key={option.value}
className={({ active, selected }) =>
`${
active || selected
? "text-white bg-theme"
: "text-gray-900"
} ${
item.label === "Priority" && "capitalize"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={option.value}
>
{option.color && (
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: option.color }}
></span>
)}
{option.label}
</Listbox.Option>
))
) : (
<div className="text-center">No {item.label}s found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
)}
/>
)}
</div>
</div>
))}
</div>
))}
</div>
<div className="pt-3 space-y-3">
<div className="flex justify-between items-start">
<div className="flex items-center gap-x-2 text-sm basis-1/2">
<TagIcon className="w-4 h-4" />
<p>Label</p>
</div>
<div className="basis-1/2">
<div className="flex gap-1 flex-wrap">
{issueDetail?.label_details.map((label) => (
<span
key={label.id}
className="flex items-center gap-2 border rounded-2xl text-xs px-1 py-0.5 hover:bg-gray-100 cursor-pointer"
// onClick={() =>
// submitChanges({
// labels_list: issueDetail?.labels_list.filter((l) => l !== label.id),
// })
// }
>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: label.colour ?? "green" }}
></span>
{label.name}
</span>
))}
<Controller
control={control}
name="labels_list"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
multiple
onChange={(value: any) => submitChanges({ labels_list: value })}
className="flex-shrink-0"
>
{({ open }) => (
<>
<Listbox.Label className="sr-only">Label</Listbox.Label>
<div className="relative">
<Listbox.Button className="flex items-center gap-2 border rounded-2xl text-xs px-1 py-0.5 hover:bg-gray-100 cursor-pointer">
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden sm:block text-left"
)}
>
Select Label
</span>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1">
{issueLabels ? (
issueLabels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => (
<Listbox.Option
key={label.id}
className={({ active, selected }) =>
`${
active || selected
? "text-white bg-theme"
: "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={label.id}
>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: label.colour ?? "green" }}
></span>
{label.name}
</Listbox.Option>
))
) : (
<div className="text-center">No labels found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)}
/>
</div>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
onClick={() => setCreateLabelForm((prevData) => !prevData)}
>
{createLabelForm ? (
<>
<XMarkIcon className="h-3 w-3" /> Cancel
</>
) : (
<>
<PlusIcon className="h-3 w-3" /> Create new
</>
)}
</Popover>
</button>
</div>
<Input
id="name"
name="name"
placeholder="Title"
register={register}
validations={{
required: "This is required",
}}
autoComplete="off"
/>
<Button type="submit" disabled={isSubmitting}>
+
</Button>
</form>
<div className="flex justify-between items-center">
<div className="flex items-center gap-x-2 text-sm basis-1/2">
<TagIcon className="w-4 h-4" />
<p>Label</p>
</div>
<div className="basis-1/2">
<Controller
control={control}
name="labels_list"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
multiple
onChange={(value: any) => submitChanges({ labels_list: value })}
className="flex-shrink-0"
>
{createLabelForm && (
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(onSubmit)}>
<div>
<Popover className="relative">
{({ open }) => (
<>
<Listbox.Label className="sr-only">Label</Listbox.Label>
<div className="relative">
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<Popover.Button
className={`bg-white flex items-center gap-1 rounded-md p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
>
{watch("colour") && watch("colour") !== "" && (
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden truncate capitalize sm:block text-left"
)}
>
{value && value.length > 0
? value
.map(
(i: string) =>
issueLabels?.find((option) => option.id === i)?.name
)
.join(", ")
: "None"}
</span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button>
className="w-5 h-5 rounded"
style={{
backgroundColor: watch("colour") ?? "green",
}}
></span>
)}
<ChevronDownIcon className="h-3 w-3" />
</Popover.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1">
{issueLabels ? (
issueLabels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => (
<Listbox.Option
key={label.id}
className={({ active, selected }) =>
`${
active || selected
? "text-white bg-theme"
: "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={label.id}
>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: label.colour ?? "green" }}
></span>
{label.name}
</Listbox.Option>
))
) : (
<div className="text-center">No labels found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
<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 z-10 transform right-0 mt-1 px-2 max-w-xs sm:px-0">
<Controller
name="colour"
control={controlLabel}
render={({ field: { value, onChange } }) => (
<TwitterPicker
color={value}
onChange={(value) => onChange(value.hex)}
/>
)}
/>
</Popover.Panel>
</Transition>
</>
)}
</Listbox>
)}
/>
</div>
</Popover>
</div>
<Input
id="name"
name="name"
placeholder="Title"
register={register}
validations={{
required: "This is required",
}}
autoComplete="off"
/>
<Button type="submit" theme="success" disabled={isSubmitting}>
+
</Button>
</form>
)}
</div>
</div>
</div>
</>
);
};

View File

@ -24,12 +24,12 @@ type Props = {
const activityIcons: {
[key: string]: JSX.Element;
} = {
state: <Squares2X2Icon className="h-4 w-4" />,
priority: <ChartBarIcon className="h-4 w-4" />,
name: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
description: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
target_date: <CalendarDaysIcon className="h-4 w-4" />,
parent: <UserIcon className="h-4 w-4" />,
state: <Squares2X2Icon className="h-3.5 w-3.5" />,
priority: <ChartBarIcon className="h-3.5 w-3.5" />,
name: <ChatBubbleBottomCenterTextIcon className="h-3.5 w-3.5" />,
description: <ChatBubbleBottomCenterTextIcon className="h-3.5 w-3.5" />,
target_date: <CalendarDaysIcon className="h-3.5 w-3.5" />,
parent: <UserIcon className="h-3.5 w-3.5" />,
};
const IssueActivitySection: React.FC<Props> = ({ issueActivities, states, issues }) => {

View File

@ -77,42 +77,6 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
return (
<div className="space-y-5">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="bg-gray-100 rounded-md">
<div className="w-full">
<TextArea
id="comment"
name="comment"
register={register}
validations={{
required: true,
}}
mode="transparent"
error={errors.comment}
className="w-full pb-10 resize-none"
placeholder="Enter your comment"
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) {
e.preventDefault();
const value = e.currentTarget.value;
const start = e.currentTarget.selectionStart;
const end = e.currentTarget.selectionEnd;
setValue("comment", `${value.substring(0, start)}\r ${value.substring(end)}`);
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
isSubmitting || handleSubmit(onSubmit)();
}
}}
/>
</div>
<div className="w-full flex justify-end">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Adding comment..." : "Add comment"}
{/* <UploadingIcon /> */}
</Button>
</div>
</div>
</form>
{comments ? (
comments.length > 0 ? (
<div className="space-y-5">
@ -133,6 +97,37 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
<Spinner />
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-start gap-2 border rounded-md p-2">
<TextArea
id="comment"
name="comment"
register={register}
validations={{
required: true,
}}
mode="transparent"
error={errors.comment}
placeholder="Enter your comment"
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) {
e.preventDefault();
const value = e.currentTarget.value;
const start = e.currentTarget.selectionStart;
const end = e.currentTarget.selectionEnd;
setValue("comment", `${value.substring(0, start)}\r ${value.substring(end)}`);
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
isSubmitting || handleSubmit(onSubmit)();
}
}}
/>
<Button type="submit" className="whitespace-nowrap" disabled={isSubmitting}>
{isSubmitting ? "Adding comment..." : "Add comment"}
{/* <UploadingIcon /> */}
</Button>
</div>
</form>
</div>
);
};

View File

@ -68,7 +68,7 @@ const ProjectMemberInvitations: React.FC<Props> = ({
{!isMember ? (
<input
id={project.id}
className="h-3 w-3 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 mt-2 hidden"
className="h-3 w-3 rounded border-gray-300 text-theme focus:ring-indigo-500 mt-2 hidden"
aria-describedby="workspaces"
name={project.id}
checked={invitationsRespond.includes(project.id)}

View File

@ -92,7 +92,7 @@ const ControlSettings: React.FC<Props> = ({ control, isSubmitting }) => {
{selected ? (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active ? "text-white" : "text-indigo-600"
active ? "text-white" : "text-theme"
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
@ -164,7 +164,7 @@ const ControlSettings: React.FC<Props> = ({ control, isSubmitting }) => {
{selected ? (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active ? "text-white" : "text-indigo-600"
active ? "text-white" : "text-theme"
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />

View File

@ -47,7 +47,6 @@ const LabelsSettings: React.FC = () => {
setValue,
formState: { errors, isSubmitting },
watch,
setError,
} = useForm<IIssueLabels>({ defaultValues });
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
@ -117,7 +116,7 @@ const LabelsSettings: React.FC = () => {
</Button>
</div>
<div className="space-y-5">
<form
<div
className={`bg-white px-4 py-2 flex items-center gap-2 ${newLabelForm ? "" : "hidden"}`}
>
<div>
@ -193,7 +192,7 @@ const LabelsSettings: React.FC = () => {
{isSubmitting ? "Adding" : "Add"}
</Button>
)}
</form>
</div>
{issueLabels ? (
issueLabels.map((label) => {
const children = getLabelChildren(label.id);

View File

@ -0,0 +1,193 @@
// react
import React, { useState } from "react";
// next
import { useRouter } from "next/router";
import Link from "next/link";
// hooks
import useToast from "lib/hooks/useToast";
import useUser from "lib/hooks/useUser";
// components
import CreateProjectModal from "components/project/create-project-modal";
// headless ui
import { Disclosure, Menu, Transition } from "@headlessui/react";
// ui
import { Spinner } from "ui";
// icons
import {
ChevronDownIcon,
ClipboardDocumentIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// constants
import { classNames, copyTextToClipboard } from "constants/common";
type Props = {
navigation: (projectId: string) => Array<{
name: string;
href: string;
icon: (props: any) => JSX.Element;
}>;
sidebarCollapse: boolean;
};
const ProjectsList: React.FC<Props> = ({ navigation, sidebarCollapse }) => {
const [isCreateProjectModal, setCreateProjectModal] = useState(false);
const { projects } = useUser();
const { setToastAlert } = useToast();
const router = useRouter();
const { projectId } = router.query;
return (
<>
<CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} />
<div
className={`h-full flex flex-col px-2 pt-5 pb-3 mt-3 space-y-2 bg-primary overflow-y-auto ${
sidebarCollapse ? "rounded-xl" : "rounded-t-3xl"
}`}
>
{projects ? (
<>
{projects.length > 0 ? (
projects.map((project) => (
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
{({ open }) => (
<>
<div className="flex items-center">
<Disclosure.Button
className={`w-full flex items-center gap-2 font-medium rounded-md p-2 text-sm ${
sidebarCollapse ? "justify-center" : ""
}`}
>
<span className="bg-gray-700 text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
{project?.name.charAt(0)}
</span>
{!sidebarCollapse && (
<span className="flex items-center justify-between w-full">
{project?.name}
<span>
<ChevronDownIcon
className={`h-4 w-4 duration-300 ${open ? "rotate-180" : ""}`}
/>
</span>
</span>
)}
</Disclosure.Button>
{!sidebarCollapse && (
<Menu as="div" className="relative inline-block">
<Menu.Button className="grid relative place-items-center focus:outline-none">
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
<div className="p-1">
<Menu.Item as="div">
{(active) => (
<button
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/projects/${project?.id}/issues/`
).then(() => {
setToastAlert({
title: "Link Copied",
message: "Link copied to clipboard",
type: "success",
});
})
}
>
<ClipboardDocumentIcon className="h-3 w-3" />
Copy Link
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)}
</div>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel
className={`${
sidebarCollapse ? "" : "ml-[2.25rem]"
} flex flex-col gap-y-1`}
>
{navigation(project?.id).map((item) => (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.href === router.asPath
? "bg-gray-200 text-gray-900"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900",
"group flex items-center px-2 py-2 text-xs font-medium rounded-md outline-none",
sidebarCollapse ? "justify-center" : ""
)}
>
<item.icon
className={classNames(
item.href === router.asPath
? "text-gray-900"
: "text-gray-500 group-hover:text-gray-900",
"flex-shrink-0 h-4 w-4",
!sidebarCollapse ? "mr-3" : ""
)}
aria-hidden="true"
/>
{!sidebarCollapse && item.name}
</a>
</Link>
))}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
))
) : (
<div className="text-center space-y-3">
{!sidebarCollapse && (
<h4 className="text-gray-700 text-sm">You don{"'"}t have any project yet</h4>
)}
<button
type="button"
className="group flex justify-center items-center gap-2 w-full rounded-md p-2 text-sm bg-theme text-white"
onClick={() => setCreateProjectModal(true)}
>
<PlusIcon className="h-5 w-5" />
{!sidebarCollapse && "Create Project"}
</button>
</div>
)}
</>
) : (
<div className="w-full flex justify-center">
<Spinner />
</div>
)}
</div>
</>
);
};
export default ProjectsList;

View File

@ -0,0 +1,293 @@
// react
import React from "react";
// next
import { useRouter } from "next/router";
import Link from "next/link";
import Image from "next/image";
// services
import userService from "lib/services/user.service";
import authenticationService from "lib/services/authentication.service";
// hooks
import useUser from "lib/hooks/useUser";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// ui
import { Spinner } from "ui";
// icons
import {
ChevronDownIcon,
ClipboardDocumentListIcon,
Cog6ToothIcon,
HomeIcon,
PlusIcon,
RectangleStackIcon,
UserGroupIcon,
UserIcon,
} from "@heroicons/react/24/outline";
// types
import { IUser } from "types";
type Props = {
sidebarCollapse: boolean;
};
const workspaceLinks = [
{
icon: HomeIcon,
name: "Home",
href: `/workspace`,
},
{
icon: ClipboardDocumentListIcon,
name: "Projects",
href: "/projects",
},
{
icon: RectangleStackIcon,
name: "My Issues",
href: "/me/my-issues",
},
{
icon: UserGroupIcon,
name: "Members",
href: "/workspace/members",
},
// {
// icon: InboxIcon,
// name: "Inbox",
// href: "#",
// },
{
icon: Cog6ToothIcon,
name: "Settings",
href: "/workspace/settings",
},
];
const userLinks = [
{
name: "My Profile",
href: "/me/profile",
},
{
name: "Workspace Invites",
href: "/invitations",
},
];
const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
const { workspaces, activeWorkspace, user, mutateUser } = useUser();
const router = useRouter();
return (
<div className="px-2">
<div
className={`relative ${sidebarCollapse ? "flex" : "grid grid-cols-5 gap-2 items-center"}`}
>
<Menu as="div" className="col-span-4 inline-block text-left w-full">
<div className="w-full">
<Menu.Button
className={`inline-flex justify-between items-center w-full rounded-md px-2 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${
!sidebarCollapse
? "hover:bg-gray-50 focus:bg-gray-50 border border-gray-300 shadow-sm"
: ""
}`}
>
<div className="flex gap-x-1 items-center">
<div className="h-5 w-5 p-4 flex items-center justify-center bg-gray-700 text-white rounded uppercase relative">
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
<Image
src={activeWorkspace.logo}
alt="Workspace Logo"
layout="fill"
objectFit="cover"
/>
) : (
activeWorkspace?.name?.charAt(0) ?? "N"
)}
</div>
{!sidebarCollapse && (
<p className="truncate w-20 text-left ml-1">
{activeWorkspace?.name ?? "Loading..."}
</p>
)}
</div>
{!sidebarCollapse && (
<div className="flex-grow flex justify-end">
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</div>
)}
</Menu.Button>
</div>
<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="origin-top-left fixed max-w-[15rem] ml-2 left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
<div className="p-1">
{workspaces ? (
<>
{workspaces.length > 0 ? (
workspaces.map((workspace: any) => (
<Menu.Item key={workspace.id}>
{({ active }) => (
<button
type="button"
onClick={() => {
mutateUser(
(prevData) => ({
...(prevData as IUser),
last_workspace_id: workspace.id,
}),
false
);
userService
.updateUser({
last_workspace_id: workspace?.id,
})
.then((res) => {
const isInProject = router.pathname.includes("/[projectId]/");
if (isInProject) router.push("/workspace");
})
.catch((err) => console.error(err));
}}
className={`${
active ? "bg-theme text-white" : "text-gray-900"
} group flex w-full items-center rounded-md p-2 text-sm`}
>
{workspace.name}
</button>
)}
</Menu.Item>
))
) : (
<p>No workspace found!</p>
)}
<Menu.Item
as="button"
onClick={() => {
router.push("/create-workspace");
}}
className="w-full"
>
{({ active }) => (
<a
className={`flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm ${
active ? "bg-theme text-white" : "text-gray-900"
}`}
>
<PlusIcon className="w-5 h-5" />
<span>Create Workspace</span>
</a>
)}
</Menu.Item>
</>
) : (
<div className="w-full flex justify-center">
<Spinner />
</div>
)}
</div>
</Menu.Items>
</Transition>
</Menu>
{!sidebarCollapse && (
<Menu as="div" className="inline-block text-left flex-shrink-0 w-full">
<div className="h-10 w-10">
<Menu.Button className="h-full w-full grid relative place-items-center rounded-md shadow-sm bg-white text-gray-700 hover:bg-gray-50 focus:outline-none">
{user?.avatar && user.avatar !== "" ? (
<Image src={user.avatar} alt="User Avatar" layout="fill" className="rounded-md" />
) : (
<UserIcon className="h-5 w-5" />
)}
</Menu.Button>
</div>
<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="origin-top-right absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
<div className="p-1">
{userLinks.map((item) => (
<Menu.Item key={item.name} as="div">
{(active) => (
<Link href={item.href}>
<a className="flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm">
{item.name}
</a>
</Link>
)}
</Menu.Item>
))}
<Menu.Item as="div">
<button
type="button"
className="flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm"
onClick={async () => {
await authenticationService
.signOut({
refresh_token: authenticationService.getRefreshToken(),
})
.then((response) => {
console.log("user signed out", response);
})
.catch((error) => {
console.log("Failed to sign out", error);
})
.finally(() => {
mutateUser();
router.push("/signin");
});
}}
>
Sign Out
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)}
</div>
<div className="mt-3 flex-1 space-y-1 bg-white">
{workspaceLinks.map((link, index) => (
<Link key={index} href={link.href}>
<a
className={`${
link.href === router.asPath
? "bg-theme text-white"
: "hover:bg-indigo-100 focus:bg-indigo-100"
} flex items-center gap-3 p-2 text-xs font-medium rounded-md outline-none ${
sidebarCollapse ? "justify-center" : ""
}`}
>
<link.icon
className={`${
link.href === router.asPath ? "text-white" : ""
} flex-shrink-0 h-4 w-4`}
aria-hidden="true"
/>
{!sidebarCollapse && link.name}
</a>
</Link>
))}
</div>
</div>
);
};
export default WorkspaceOptions;

View File

@ -65,7 +65,7 @@ const SingleInvitation: React.FC<Props> = ({
setIsChecked(e.target.checked);
}}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="h-4 w-4 rounded border-gray-300 text-theme focus:ring-indigo-500"
/>
</div>
</label>

View File

@ -1,30 +0,0 @@
import React, { useState } from "react";
// layouts
import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/Sidebar";
// components
import CreateProjectModal from "components/project/create-project-modal";
// types
import type { Props } from "./types";
const AppLayout: React.FC<Props> = ({ meta, children, noPadding = false, bg = "primary" }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Container meta={meta}>
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="h-screen w-full flex overflow-x-hidden">
<Sidebar />
<main
className={`h-full w-full min-w-0 overflow-y-auto ${noPadding ? "" : "p-5"} ${
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
}`}
>
{children}
</main>
</div>
</Container>
);
};
export default AppLayout;

View File

@ -15,16 +15,14 @@ const DefaultTopBar: React.FC = () => {
<a className="flex">
<span className="sr-only">Plane</span>
<h2 className="text-2xl font-semibold">
Plan<span className="text-indigo-600">e</span>
Plan<span className="text-theme">e</span>
</h2>
</a>
</Link>
</div>
{user && (
<div>
<p className="text-sm text-gray-500">
logged in as {user.first_name}
</p>
<p className="text-sm text-gray-500">logged in as {user.first_name}</p>
</div>
)}
</div>

View File

@ -0,0 +1,21 @@
type Props = {
breadcrumbs?: JSX.Element;
left?: JSX.Element;
right?: JSX.Element;
};
const Header: React.FC<Props> = ({ breadcrumbs, left, right }) => {
return (
<>
<div className="w-full bg-gray-50 border-b border-gray-200 flex justify-between items-center px-5 py-4">
<div className="flex items-center gap-2">
{breadcrumbs}
{left}
</div>
{right}
</div>
</>
);
};
export default Header;

View File

@ -0,0 +1,201 @@
import React, { useState } from "react";
// next
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import useTheme from "lib/hooks/useTheme";
// components
import ProjectsList from "components/sidebar/projects-list";
import WorkspaceOptions from "components/sidebar/workspace-options";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// icons
import {
ArrowPathIcon,
Bars3Icon,
Cog6ToothIcon,
RectangleStackIcon,
UserGroupIcon,
XMarkIcon,
ArrowLongLeftIcon,
QuestionMarkCircleIcon,
} from "@heroicons/react/24/outline";
// constants
import { classNames } from "constants/common";
const navigation = (projectId: string) => [
{
name: "Issues",
href: `/projects/${projectId}/issues`,
icon: RectangleStackIcon,
},
{
name: "Cycles",
href: `/projects/${projectId}/cycles`,
icon: ArrowPathIcon,
},
{
name: "Members",
href: `/projects/${projectId}/members`,
icon: UserGroupIcon,
},
{
name: "Settings",
href: `/projects/${projectId}/settings`,
icon: Cog6ToothIcon,
},
];
const Sidebar: React.FC = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const router = useRouter();
const { projectId } = router.query;
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
return (
<nav className="h-screen">
<Transition.Root show={sidebarOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-40 md:hidden" onClose={setSidebarOpen}>
<Transition.Child
as={React.Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
</Transition.Child>
<div className="fixed inset-0 z-40 flex">
<Transition.Child
as={React.Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white">
<Transition.Child
as={React.Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
type="button"
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</Transition.Child>
<div className="h-0 flex-1 overflow-y-auto pt-5 pb-4">
<nav className="mt-5 space-y-1 px-2">
{projectId &&
navigation(projectId as string).map((item) => (
<Link href={item.href} key={item.name}>
<a
className={classNames(
item.href === router.asPath
? "bg-gray-100 text-gray-900"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
)}
>
<item.icon
className={classNames(
item.href === router.asPath
? "text-gray-500"
: "text-gray-400 group-hover:text-gray-500",
"mr-4 flex-shrink-0 h-6 w-6"
)}
aria-hidden="true"
/>
{item.name}
</a>
</Link>
))}
</nav>
</div>
</Dialog.Panel>
</Transition.Child>
<div className="w-14 flex-shrink-0" />
</div>
</Dialog>
</Transition.Root>
<div
className={`${
sidebarCollapse ? "" : "w-auto md:w-64"
} h-full hidden md:inset-y-0 md:flex md:flex-col`}
>
<div className="h-full flex flex-1 flex-col border-r border-gray-200">
<div className="h-full flex flex-1 flex-col pt-2">
<WorkspaceOptions sidebarCollapse={sidebarCollapse} />
<ProjectsList navigation={navigation} sidebarCollapse={sidebarCollapse} />
<div
className={`px-2 py-2 w-full self-baseline flex items-center bg-primary ${
sidebarCollapse ? "flex-col-reverse" : ""
}`}
>
<button
type="button"
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
sidebarCollapse ? "justify-center w-full" : ""
}`}
onClick={() => toggleCollapsed()}
>
<ArrowLongLeftIcon
className={`h-4 w-4 text-gray-500 group-hover:text-gray-900 flex-shrink-0 duration-300 ${
sidebarCollapse ? "rotate-180" : ""
}`}
/>
</button>
<button
type="button"
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
sidebarCollapse ? "justify-center w-full" : ""
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "h",
});
document.dispatchEvent(e);
}}
title="Help"
>
<QuestionMarkCircleIcon className="h-4 w-4 text-gray-500" />
</button>
</div>
</div>
</div>
</div>
<div className="sticky top-0 z-10 bg-white pl-1 pt-1 sm:pl-3 sm:pt-3 md:hidden">
<button
type="button"
className="-ml-0.5 -mt-0.5 inline-flex h-12 w-12 items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</nav>
);
};
export default Sidebar;

View File

@ -0,0 +1,41 @@
// next
import Link from "next/link";
import { useRouter } from "next/router";
type Props = {
links: Array<{
label: string;
href: string;
}>;
};
const SettingsSidebar: React.FC<Props> = ({ links }) => {
const router = useRouter();
return (
<nav className="h-screen w-72 border-r border-gray-200">
<div className="h-full p-2 pt-4">
<h2 className="text-lg font-medium leading-5">Settings</h2>
<div className="mt-3">
{links.map((link, index) => (
<h4 key={index}>
<Link href={link.href}>
<a
className={`${
link.href === router.asPath
? "bg-theme text-white"
: "hover:bg-indigo-100 focus:bg-indigo-100"
} flex items-center gap-3 p-2 text-xs font-medium rounded-md outline-none`}
>
{link.label}
</a>
</Link>
</h4>
))}
</div>
</div>
</nav>
);
};
export default SettingsSidebar;

View File

@ -0,0 +1,41 @@
import React, { useState } from "react";
// layouts
import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/main-sidebar";
import Header from "layouts/Navbar/Header";
// components
import CreateProjectModal from "components/project/create-project-modal";
// types
import type { Props } from "./types";
const AppLayout: React.FC<Props> = ({
meta,
children,
noPadding = false,
bg = "primary",
breadcrumbs,
right,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Container meta={meta}>
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="h-screen w-full flex overflow-x-hidden">
<Sidebar />
<main className="h-screen w-full flex flex-col overflow-y-auto min-w-0">
<Header breadcrumbs={breadcrumbs} right={right} />
<div
className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
}`}
>
{children}
</div>
</main>
</div>
</Container>
);
};
export default AppLayout;

View File

@ -0,0 +1,70 @@
// react
import React, { useEffect, useState } from "react";
// next
import { useRouter } from "next/router";
// hooks
import useUser from "lib/hooks/useUser";
// layouts
import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/main-sidebar";
import SettingsSidebar from "layouts/Navbar/settings-sidebar";
import Header from "layouts/Navbar/Header";
// components
import CreateProjectModal from "components/project/create-project-modal";
// types
import { Meta } from "./types";
type Props = {
meta?: Meta;
children: React.ReactNode;
noPadding?: boolean;
bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element;
right?: JSX.Element;
links: Array<{
label: string;
href: string;
}>;
};
const SettingsLayout: React.FC<Props> = ({
meta,
children,
noPadding = false,
bg = "primary",
breadcrumbs,
right,
links,
}) => {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const { user, isUserLoading } = useUser();
useEffect(() => {
if (!isUserLoading && (!user || user === null)) router.push("/signin");
}, [isUserLoading, user, router]);
return (
<Container meta={meta}>
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="h-screen w-full flex overflow-x-hidden">
<Sidebar />
<SettingsSidebar links={links} />
<main className="h-screen w-full flex flex-col overflow-y-auto min-w-0">
<Header breadcrumbs={breadcrumbs} right={right} />
<div
className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
}`}
>
{children}
</div>
</main>
</div>
</Container>
);
};
export default SettingsLayout;

View File

@ -10,4 +10,6 @@ export type Props = {
children: React.ReactNode;
noPadding?: boolean;
bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element;
right?: JSX.Element;
};

View File

@ -6,9 +6,9 @@ import { groupBy, orderArrayBy } from "constants/common";
// constants
import { PRIORITIES } from "constants/";
// types
import type { IssueResponse, IIssue } from "types";
import type { IIssue } from "types";
const useIssuesFilter = (projectIssues?: IssueResponse) => {
const useIssuesFilter = (projectIssues: IIssue[]) => {
const {
issueView,
setIssueView,
@ -31,18 +31,18 @@ const useIssuesFilter = (projectIssues?: IssueResponse) => {
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues?.results.filter((issue) => issue.state === state.name) ?? [],
projectIssues.filter((issue) => issue.state === state.name) ?? [],
]) ?? []
)
: groupByProperty === "priority"
? Object.fromEntries(
PRIORITIES.map((priority) => [
priority,
projectIssues?.results.filter((issue) => issue.priority === priority) ?? [],
projectIssues.filter((issue) => issue.priority === priority) ?? [],
])
)
: {}),
...groupBy(projectIssues?.results ?? [], groupByProperty ?? ""),
...groupBy(projectIssues ?? [], groupByProperty ?? ""),
};
if (orderBy !== null) {
@ -64,7 +64,7 @@ const useIssuesFilter = (projectIssues?: IssueResponse) => {
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues?.results.filter((issue) => issue.state === state.id) ?? [],
projectIssues.filter((issue) => issue.state === state.id) ?? [],
]) ?? []
);
} else if (filterIssue === "backlogIssue") {
@ -76,7 +76,7 @@ const useIssuesFilter = (projectIssues?: IssueResponse) => {
?.sort((a, b) => a.sequence - b.sequence)
?.map((state) => [
state.name,
projectIssues?.results.filter((issue) => issue.state === state.id) ?? [],
projectIssues.filter((issue) => issue.state === state.id) ?? [],
]) ?? []
);
}

View File

@ -88,7 +88,7 @@ class ProjectIssuesServices extends APIService {
});
}
async addIssueToSprint(
async addIssueToCycle(
workspaceSlug: string,
projectId: string,
cycleId: string,

View File

@ -19,12 +19,7 @@ import APIService from "lib/services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
// types
import {
ILastActiveWorkspaceDetails,
IWorkspace,
IWorkspaceMember,
IWorkspaceMemberInvitation,
} from "types";
import { IWorkspace, IWorkspaceMember, IWorkspaceMemberInvitation } from "types";
class WorkspaceService extends APIService {
constructor() {
@ -103,16 +98,6 @@ class WorkspaceService extends APIService {
});
}
async getLastActiveWorkspaceAndProjects(): Promise<ILastActiveWorkspaceDetails> {
return this.get(LAST_ACTIVE_WORKSPACE_AND_PROJECTS)
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async userWorkspaceInvitations(): Promise<IWorkspaceMemberInvitation[]> {
return this.get(USER_WORKSPACE_INVITATIONS)
.then((response) => {

View File

@ -68,7 +68,7 @@ const CreateWorkspace: NextPage = () => {
<DefaultLayout>
<div className="flex flex-col items-center justify-center w-full h-full px-4">
{user && (
<div className="w-96 p-2 rounded-lg bg-indigo-100 text-indigo-600 mb-10 lg:mb-20">
<div className="w-96 p-2 rounded-lg bg-indigo-100 text-theme mb-10 lg:mb-20">
<p className="text-sm text-center">logged in as {user.email}</p>
</div>
)}

View File

@ -82,7 +82,7 @@ const OnBoard: NextPage = () => {
>
<div className="flex min-h-full flex-col items-center justify-center p-4 sm:p-0">
{user && (
<div className="w-96 p-2 rounded-lg bg-indigo-100 text-indigo-600 mb-10">
<div className="w-96 p-2 rounded-lg bg-indigo-100 text-theme mb-10">
<p className="text-sm text-center">logged in as {user.email}</p>
</div>
)}
@ -120,7 +120,7 @@ const OnBoard: NextPage = () => {
);
}}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="h-4 w-4 rounded border-gray-300 text-theme focus:ring-indigo-500"
/>
<label htmlFor={item.id} className="text-sm">
Accept

View File

@ -5,7 +5,7 @@ import type { NextPage } from "next";
// swr
import useSWR from "swr";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// hooks
import useUser from "lib/hooks/useUser";
// ui
@ -71,176 +71,107 @@ const MyIssues: NextPage = () => {
});
};
const handleWorkspaceChange = (workspaceId: string | null) => {
setSelectedWorkspace(workspaceId);
};
return (
<AppLayout>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="My Issues" />
</Breadcrumbs>
}
right={
<HeaderButton
Icon={PlusIcon}
label="Add Issue"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "i",
ctrlKey: true,
});
document.dispatchEvent(e);
}}
/>
}
>
<div className="w-full h-full flex flex-col space-y-5">
{myIssues ? (
<>
{myIssues.length > 0 ? (
<>
<Breadcrumbs>
<BreadcrumbItem title="My Issues" />
</Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">My Issues</h2>
<div className="flex items-center gap-x-3">
<Menu as="div" className="relative inline-block w-40">
<div className="w-full">
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none">
<span className="flex gap-x-1 items-center">
{workspaces?.find((w) => w.id === selectedWorkspace)?.name ??
"All workspaces"}
</span>
<div className="flex-grow flex justify-end">
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</div>
</Menu.Button>
</div>
<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="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
<div className="p-1">
<Menu.Item>
{({ active }) => (
<button
type="button"
className={`${
active ? "bg-theme text-white" : "text-gray-900"
} group flex w-full items-center rounded-md p-2 text-xs`}
onClick={() => handleWorkspaceChange(null)}
>
All workspaces
</button>
<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"
)}
</Menu.Item>
{workspaces &&
workspaces.map((workspace) => (
<Menu.Item key={workspace.id}>
{({ active }) => (
<button
type="button"
className={`${
active ? "bg-theme text-white" : "text-gray-900"
} group flex w-full items-center rounded-md p-2 text-xs`}
onClick={() => handleWorkspaceChange(workspace.id)}
>
{workspace.name}
</button>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
<HeaderButton
Icon={PlusIcon}
label="Add Issue"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "i",
ctrlKey: true,
});
document.dispatchEvent(e);
}}
/>
</div>
</div>
<div className="mt-4 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>
>
<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>
</thead>
<tbody className="bg-white">
{myIssues
.filter((i) =>
selectedWorkspace ? i.workspace === selectedWorkspace : true
)
.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>
))}
</tbody>
</table>
</div>
</div>
</div>
</>
</div>
) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4">
<EmptySpace

View File

@ -15,7 +15,7 @@ import useToast from "lib/hooks/useToast";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// constants
import { USER_ISSUE, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// services

View File

@ -10,7 +10,7 @@ import useUser from "lib/hooks/useUser";
// hoc
import withAuthWrapper from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// ui
import { Button } from "ui";
// swr
@ -89,7 +89,7 @@ const MyWorkspacesInvites: NextPage = () => {
)
}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="h-4 w-4 rounded border-gray-300 text-theme focus:ring-indigo-500"
/>
</div>
<div className="ml-3 text-sm flex justify-between w-full">

View File

@ -0,0 +1,416 @@
// react
import React from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR, { mutate } from "swr";
// react-beautiful-dnd
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";
// services
import issuesServices from "lib/services/issues.service";
import cycleServices from "lib/services/cycles.service";
import projectService from "lib/services/project.service";
// hooks
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";
// 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";
// types
import { CycleIssueResponse, IIssue, NestedKeyOf, Properties } 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";
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" },
{ name: "Priority", key: "priority" },
{ name: "Created By", key: "created_by" },
{ name: "None", key: null },
];
const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
{ name: "Last created", key: "created_at" },
{ name: "Last updated", key: "updated_at" },
{ name: "Priority", key: "priority" },
];
const filterIssueOptions: Array<{
name: string;
key: "activeIssue" | "backlogIssue" | null;
}> = [
{
name: "All",
key: null,
},
{
name: "Active Issues",
key: "activeIssue",
},
{
name: "Backlog Issues",
key: "backlogIssue",
},
];
type Props = {};
const SingleCycle: React.FC<Props> = () => {
const { activeWorkspace, activeProject, cycles } = useUser();
const router = useRouter();
const { cycleId } = router.query;
const [properties, setProperties] = useIssuesProperties(
activeWorkspace?.slug,
activeProject?.id as string
);
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
activeWorkspace && activeProject && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
activeWorkspace && activeProject && cycleId
? () =>
cycleServices.getCycleIssues(activeWorkspace?.slug, activeProject?.id, cycleId as string)
: null
);
const cycleIssuesArray = cycleIssues?.map((issue) => {
return issue.issue_details;
});
const { data: members } = useSWR(
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
activeWorkspace && activeProject
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
: null,
{
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
if (err?.status === 403) return;
setTimeout(() => revalidate(revalidateOpts), 5000);
},
}
);
const {
issueView,
setIssueView,
groupByProperty,
setGroupByProperty,
groupedByIssues,
setOrderBy,
setFilterIssue,
orderBy,
filterIssue,
} = useIssuesFilter(cycleIssuesArray ?? []);
const addIssueToCycle = (cycleId: string, issueId: string) => {
if (!activeWorkspace || !activeProject?.id) return;
issuesServices
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, {
issue: issueId,
})
.then((response) => {
console.log(response);
mutate(CYCLE_ISSUES(cycleId));
})
.catch((error) => {
console.log(error);
});
};
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const { source, destination } = result;
if (source.droppableId === destination.droppableId) return;
if (activeWorkspace && activeProject) {
// remove issue from the source cycle
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(source.droppableId),
(prevData) => prevData?.filter((p) => p.id !== result.draggableId.split(",")[0]),
false
);
// add issue to the destination cycle
mutate(CYCLE_ISSUES(destination.droppableId));
issuesServices
.removeIssueFromCycle(
activeWorkspace.slug,
activeProject.id,
source.droppableId,
result.draggableId.split(",")[0]
)
.then((res) => {
issuesServices
.addIssueToCycle(activeWorkspace.slug, activeProject.id, destination.droppableId, {
issue: result.draggableId.split(",")[1],
})
.then((res) => {
console.log(res);
})
.catch((e) => {
console.log(e);
});
})
.catch((e) => {
console.log(e);
});
}
// console.log(result);
};
const removeIssueFromCycle = (cycleId: string, bridgeId: string) => {
if (activeWorkspace && activeProject) {
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId),
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
false
);
issuesServices
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId)
.then((res) => {
console.log(res);
})
.catch((e) => {
console.log(e);
});
}
};
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 }) => (
<>
<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 : (
<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>
</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
groupedByIssues={groupedByIssues}
properties={properties}
removeIssueFromCycle={removeIssueFromCycle}
selectedGroup={groupByProperty}
members={members}
openCreateIssueModal={() => {
return;
}}
openIssuesListModal={() => {
return;
}}
/>
</div>
)}
</AppLayout>
);
};
export default SingleCycle;

View File

@ -1,34 +1,39 @@
// react
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, { mutate } from "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";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// fetching keys
import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys";
import { CYCLE_LIST } from "constants/fetch-keys";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// components
import CycleView from "components/project/cycles/CycleView";
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
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";
// ui
import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySpaceItem } from "ui";
// icons
import { PlusIcon } from "@heroicons/react/20/solid";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { ArrowPathIcon, ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, ICycle, SelectSprintType, SelectIssue, CycleIssueResponse } from "types";
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import { IIssue, ICycle, SelectSprintType, SelectIssue, Properties } from "types";
// constants
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
const ProjectSprints: NextPage = () => {
const [isOpen, setIsOpen] = useState(false);
@ -37,6 +42,8 @@ const ProjectSprints: NextPage = () => {
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();
@ -45,13 +52,18 @@ const ProjectSprints: NextPage = () => {
const { projectId } = router.query;
const { data: cycles } = useSWR<ICycle[]>(
projectId && activeWorkspace ? CYCLE_LIST(projectId as string) : null,
activeWorkspace && projectId ? CYCLE_LIST(projectId as string) : null,
activeWorkspace && projectId
? () => sprintService.getCycles(activeWorkspace.slug, projectId as string)
: null
);
const openIssueModal = (
const [properties, setProperties] = useIssuesProperties(
activeWorkspace?.slug,
projectId as string
);
const openCreateIssueModal = (
cycleId: string,
issue?: IIssue,
actionType: "create" | "edit" | "delete" = "create"
@ -67,89 +79,9 @@ const ProjectSprints: NextPage = () => {
}
};
const addIssueToSprint = (cycleId: string, issueId: string) => {
if (!activeWorkspace || !projectId) return;
issuesServices
.addIssueToSprint(activeWorkspace.slug, projectId as string, cycleId, {
issue: issueId,
})
.then((response) => {
console.log(response);
mutate(CYCLE_ISSUES(cycleId));
})
.catch((error) => {
console.log(error);
});
};
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const { source, destination } = result;
if (source.droppableId === destination.droppableId) return;
if (activeWorkspace && activeProject) {
// remove issue from the source cycle
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(source.droppableId),
(prevData) => prevData?.filter((p) => p.id !== result.draggableId.split(",")[0]),
false
);
// add issue to the destination cycle
mutate(CYCLE_ISSUES(destination.droppableId));
// mutate<CycleIssueResponse[]>(
// CYCLE_ISSUES(destination.droppableId),
// (prevData) => {
// const issueDetails = issues?.results.find(
// (i) => i.id === result.draggableId.split(",")[1]
// );
// const targetResponse = prevData?.find((t) => t.cycle === destination.droppableId);
// console.log(issueDetails, targetResponse, prevData);
// if (targetResponse) {
// console.log("if");
// targetResponse.issue_details = issueDetails as IIssue;
// return prevData;
// } else {
// console.log("else");
// return [
// ...(prevData ?? []),
// {
// cycle: destination.droppableId,
// issue_details: issueDetails,
// } as CycleIssueResponse,
// ];
// }
// },
// false
// );
issuesServices
.removeIssueFromCycle(
activeWorkspace.slug,
activeProject.id,
source.droppableId,
result.draggableId.split(",")[0]
)
.then((res) => {
issuesServices
.addIssueToSprint(activeWorkspace.slug, activeProject.id, destination.droppableId, {
issue: result.draggableId.split(",")[1],
})
.then((res) => {
console.log(res);
})
.catch((e) => {
console.log(e);
});
})
.catch((e) => {
console.log(e);
});
}
// console.log(result);
const openIssuesListModal = (cycleId: string) => {
setCycleId(cycleId);
setCycleIssuesListModal(true);
};
useEffect(() => {
@ -171,6 +103,66 @@ const ProjectSprints: NextPage = () => {
meta={{
title: "Plane - Cycles",
}}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
</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>
}
>
<CreateUpdateSprintsModal
isOpen={
@ -203,32 +195,20 @@ const ProjectSprints: NextPage = () => {
setIsOpen={setIsOpen}
projectId={projectId as string}
/>
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycleId}
/>
{cycles ? (
cycles.length > 0 ? (
<div className="h-full w-full space-y-5">
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
</Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Project Cycle</h2>
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
</div>
<div className="space-y-5">
<DragDropContext onDragEnd={handleDragEnd}>
{cycles.map((cycle) => (
<CycleView
key={cycle.id}
cycle={cycle}
selectSprint={setSelectedSprint}
projectId={projectId as string}
workspaceSlug={activeWorkspace?.slug as string}
openIssueModal={openIssueModal}
addIssueToSprint={addIssueToSprint}
/>
))}
</DragDropContext>
</div>
<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>
))}
</div>
) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4">

View File

@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useState } from "react";
// swr
import useSWR, { mutate } from "swr";
// react hook form
import { Controller, useForm } from "react-hook-form";
import { useForm, Controller } from "react-hook-form";
// headless ui
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
// services
@ -17,14 +17,13 @@ import {
PROJECT_ISSUES_ACTIVITY,
PROJECT_ISSUES_COMMENTS,
PROJECT_ISSUES_LIST,
STATE_LIST,
} from "constants/fetch-keys";
// hooks
import useUser from "lib/hooks/useUser";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// components
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
@ -49,13 +48,14 @@ import {
} 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,
});
const IssueDetail: NextPage = () => {
const router = useRouter();
const { issueId, projectId } = router.query;
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
@ -67,19 +67,18 @@ const IssueDetail: NextPage = () => {
>(undefined);
const [issueDescriptionValue, setIssueDescriptionValue] = useState("");
const router = useRouter();
const { issueId, projectId } = router.query;
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
const handleDescriptionChange: any = (value: any) => {
console.log(value);
setIssueDescriptionValue(value);
};
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
ssr: false,
});
const LexicalViewer = dynamic(() => import("components/lexical/viewer"), {
ssr: false,
});
const {
register,
formState: { errors },
@ -143,8 +142,13 @@ const IssueDetail: NextPage = () => {
false
);
const payload = {
...formData,
// description: formData.description ? JSON.parse(formData.description) : null,
};
issuesServices
.patchIssue(activeWorkspace.slug, projectId as string, issueId as string, formData)
.patchIssue(activeWorkspace.slug, projectId as string, issueId as string, payload)
.then((response) => {
console.log(response);
})
@ -159,6 +163,7 @@ const IssueDetail: NextPage = () => {
if (issueDetail)
reset({
...issueDetail,
// description: JSON.stringify(issueDetail.description),
blockers_list:
issueDetail.blockers_list ??
issueDetail.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id),
@ -208,8 +213,50 @@ const IssueDetail: NextPage = () => {
}
};
// console.log(issueDetail);
return (
<AppLayout noPadding={true} bg="secondary">
<AppLayout
noPadding={true}
bg="secondary"
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Issues`}
link={`/projects/${activeProject?.id}/issues`}
/>
<BreadcrumbItem
title={`Issue ${activeProject?.identifier ?? "Project"}-${
issueDetail?.sequence_id ?? "..."
} Details`}
/>
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<HeaderButton
Icon={ChevronLeftIcon}
label="Previous"
className={`${!prevIssue ? "cursor-not-allowed opacity-70" : ""}`}
onClick={() => {
if (!prevIssue) return;
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
}}
/>
<HeaderButton
Icon={ChevronRightIcon}
disabled={!nextIssue}
label="Next"
className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`}
onClick={() => {
if (!nextIssue) return;
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
}}
position="reverse"
/>
</div>
}
>
<CreateUpdateIssuesModal
isOpen={isOpen}
setIsOpen={setIsOpen}
@ -218,6 +265,11 @@ const IssueDetail: NextPage = () => {
...preloadedData,
}}
/>
<ConfirmIssueDeletion
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetail}
/>
<AddAsSubIssue
isOpen={isAddAsSubIssueOpen}
setIsOpen={setIsAddAsSubIssueOpen}
@ -226,19 +278,7 @@ const IssueDetail: NextPage = () => {
{issueDetail && activeProject ? (
<div className="flex gap-5">
<div className="basis-3/4 space-y-5 p-5">
<div className="mb-5">
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Issues`}
link={`/projects/${activeProject?.id}/issues`}
/>
<BreadcrumbItem
title={`Issue ${activeProject?.identifier ?? "Project"}-${
issueDetail?.sequence_id ?? "..."
} Details`}
/>
</Breadcrumbs>
</div>
<div className="mb-5"></div>
<div className="rounded-lg">
{issueDetail.parent !== null && issueDetail.parent !== "" ? (
<div className="bg-gray-100 flex items-center gap-2 p-2 text-xs rounded mb-5 w-min whitespace-nowrap">
@ -332,15 +372,21 @@ const IssueDetail: NextPage = () => {
{/* <Controller
name="description"
control={control}
render={({ field }) => (
render={({ field: { value, onChange } }) => (
<RichTextEditor
{...field}
// value={JSON.stringify(issueDetail.description)}
value={value}
onChange={(val) => {
debounce(() => {
console.log("Debounce");
// handleSubmit(submitChanges)();
}, 5000)();
onChange(val);
}}
id="issueDescriptionEditor"
value={JSON.parse(issueDetail.description)}
/>
)}
/> */}
{/* <LexicalViewer id="descriptionViewer" value={JSON.parse(issueDetail.description)} /> */}
</div>
<div className="mt-2">
{subIssues && subIssues.length > 0 ? (
@ -568,34 +614,13 @@ const IssueDetail: NextPage = () => {
</Tab.Group>
</div>
</div>
<div className="basis-1/4 rounded-lg space-y-5 p-5 border-l">
<div className="flex justify-end items-center gap-x-3 mb-5">
<HeaderButton
Icon={ChevronLeftIcon}
label="Previous"
className={`${!prevIssue ? "cursor-not-allowed opacity-70" : ""}`}
onClick={() => {
if (!prevIssue) return;
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
}}
/>
<HeaderButton
Icon={ChevronRightIcon}
disabled={!nextIssue}
label="Next"
className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`}
onClick={() => {
if (!nextIssue) return;
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
}}
position="reverse"
/>
</div>
<div className="h-full basis-1/4 space-y-5 p-5 border-l">
<IssueDetailSidebar
control={control}
issueDetail={issueDetail}
submitChanges={submitChanges}
watch={watch}
setDeleteIssueModal={setDeleteIssueModal}
/>
</div>
</div>

View File

@ -3,11 +3,13 @@ import React, { useEffect, useState } from "react";
import type { NextPage } from "next";
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// services
import issuesServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
@ -18,23 +20,31 @@ import projectService from "lib/services/project.service";
// commons
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// hooks
import useIssuesFilter from "lib/hooks/useIssuesFilter";
// components
import ListView from "components/project/issues/ListView";
import BoardView from "components/project/issues/BoardView";
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
// ui
import { Spinner, CustomMenu, BreadcrumbItem, Breadcrumbs } from "ui";
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
import HeaderButton from "ui/HeaderButton";
import {
Spinner,
CustomMenu,
BreadcrumbItem,
Breadcrumbs,
EmptySpace,
EmptySpaceItem,
HeaderButton,
} from "ui";
// icons
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
// types
import type { IIssue, Properties, NestedKeyOf } from "types";
import type { IIssue, Properties, NestedKeyOf, IssueResponse } from "types";
// fetch-keys
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" },
@ -101,6 +111,26 @@ const ProjectIssues: NextPage = () => {
}
);
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
if (!activeWorkspace || !activeProject) return;
issuesServices
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, formData)
.then((response) => {
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
(prevData) => ({
...(prevData as IssueResponse),
results:
prevData?.results.map((issue) => (issue.id === response.id ? response : issue)) ?? [],
}),
false
);
})
.catch((error) => {
console.log(error);
});
};
const {
issueView,
setIssueView,
@ -111,7 +141,7 @@ const ProjectIssues: NextPage = () => {
setFilterIssue,
orderBy,
filterIssue,
} = useIssuesFilter(projectIssues);
} = useIssuesFilter(projectIssues?.results ?? []);
useEffect(() => {
if (!isOpen) {
@ -123,7 +153,161 @@ const ProjectIssues: NextPage = () => {
}, [isOpen]);
return (
<AppLayout>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Issues`} />
</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>
<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 : (
<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>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<HeaderButton
Icon={PlusIcon}
label="Add Issue"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "i",
ctrlKey: true,
});
document.dispatchEvent(e);
}}
/>
</div>
}
>
<CreateUpdateIssuesModal
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
setIsOpen={setIsOpen}
@ -141,167 +325,6 @@ const ProjectIssues: NextPage = () => {
</div>
) : projectIssues.count > 0 ? (
<>
<div className="w-full space-y-5 mb-5">
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Issues`} />
</Breadcrumbs>
<div className="flex items-center justify-between w-full">
<h2 className="text-2xl font-medium">Project Issues</h2>
<div className="flex items-center md:gap-x-6 sm:gap-x-3">
<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 ? "text-gray-900" : "text-gray-500",
"group inline-flex items-center rounded-md bg-transparent text-xs font-medium hover:text-gray-900 focus:outline-none border border-gray-300 px-2 py-2"
)}
>
<span>View</span>
<ChevronDownIcon
className={classNames(
open ? "text-gray-600" : "text-gray-400",
"ml-2 h-4 w-4 group-hover:text-gray-500"
)}
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="flex justify-between">
<h4 className="text-base 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">
<h4 className="text-base 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">
<h4 className="text-base 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>
{Object.keys(properties).map((key) => (
<button
key={key}
type="button"
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
properties[key as keyof Properties]
? "border-indigo-600 bg-indigo-600 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>
</div>
{issueView === "list" ? (
<ListView
properties={properties}
@ -309,14 +332,17 @@ const ProjectIssues: NextPage = () => {
selectedGroup={groupByProperty}
setSelectedIssue={setSelectedIssue}
handleDeleteIssue={setDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
/>
) : (
<div className="h-full">
<div className="h-screen">
<BoardView
properties={properties}
selectedGroup={groupByProperty}
groupedByIssues={groupedByIssues}
members={members}
handleDeleteIssue={setDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
/>
</div>
)}

View File

@ -17,7 +17,7 @@ import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// components
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove";
@ -92,7 +92,15 @@ const ProjectMembers: NextPage = () => {
];
return (
<AppLayout>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Members`} />
</Breadcrumbs>
}
right={<HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />}
>
<ConfirmProjectMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => {
@ -140,14 +148,6 @@ const ProjectMembers: NextPage = () => {
</div>
) : (
<div className="h-full w-full space-y-5">
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Members`} />
</Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Invite Members</h2>
<HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />
</div>
{members && members.length === 0 ? null : (
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
<thead className="bg-gray-50">
@ -246,7 +246,7 @@ const ProjectMembers: NextPage = () => {
Active
</span>
) : (
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-gray-900 rounded-full">
Pending
</span>
)}

View File

@ -12,7 +12,7 @@ import { Tab } from "@headlessui/react";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import SettingsLayout from "layouts/settings-layout";
// service
import projectServices from "lib/services/project.service";
// hooks
@ -26,6 +26,26 @@ import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
// types
import type { IProject, IWorkspace } from "types";
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const ControlSettings = dynamic(() => import("components/project/settings/ControlSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const StatesSettings = dynamic(() => import("components/project/settings/StatesSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const LabelsSettings = dynamic(() => import("components/project/settings/LabelsSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const defaultValues: Partial<IProject> = {
name: "",
description: "",
@ -34,27 +54,6 @@ const defaultValues: Partial<IProject> = {
};
const ProjectSettings: NextPage = () => {
// FIXME: instead of using dynamic import inside component use it outside
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const ControlSettings = dynamic(() => import("components/project/settings/ControlSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const StatesSettings = dynamic(() => import("components/project/settings/StatesSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const LabelsSettings = dynamic(() => import("components/project/settings/LabelsSettings"), {
loading: () => <p>Loading...</p>,
ssr: false,
});
const {
register,
handleSubmit,
@ -133,14 +132,38 @@ const ProjectSettings: NextPage = () => {
});
};
const sidebarLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: "#",
},
{
label: "Control",
href: "#",
},
{
label: "States",
href: "#",
},
{
label: "Labels",
href: "#",
},
];
return (
<AppLayout>
<div className="space-y-5 mb-5">
<SettingsLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
</Breadcrumbs>
</div>
}
links={sidebarLinks}
>
{projectDetails ? (
<div className="space-y-3">
<Tab.Group>
@ -186,7 +209,7 @@ const ProjectSettings: NextPage = () => {
<Spinner />
</div>
)}
</AppLayout>
</SettingsLayout>
);
};

View File

@ -6,7 +6,7 @@ import useUser from "lib/hooks/useUser";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// components
import ProjectMemberInvitations from "components/project/memberInvitations";
import ConfirmProjectDeletion from "components/project/confirm-project-deletion";

View File

@ -101,7 +101,7 @@ const SignIn: NextPage = () => {
>
{isGoogleAuthenticationLoading && (
<div className="absolute top-0 left-0 w-full h-full bg-white z-50 flex items-center justify-center">
<h2 className="text-2xl text-black">Signing in with Google. Please wait...</h2>
<h2 className="text-2xl text-gray-900">Signing in with Google. Please wait...</h2>
</div>
)}
<div className="w-full h-screen flex justify-center items-center bg-gray-50 overflow-auto">

View File

@ -4,7 +4,7 @@ import Link from "next/link";
// react
import React from "react";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// swr
import useSWR from "swr";
// hooks

View File

@ -17,7 +17,7 @@ import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// hoc
import withAuthWrapper from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// components
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove";
@ -75,6 +75,12 @@ const WorkspaceInvite: NextPage = () => {
meta={{
title: "Plane - Workspace Invite",
}}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Members`} />
</Breadcrumbs>
}
right={<HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />}
>
<ConfirmWorkspaceMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
@ -132,13 +138,6 @@ const WorkspaceInvite: NextPage = () => {
</div>
) : (
<div className="w-full space-y-5">
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Members`} />
</Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Invite Members</h2>
<HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />
</div>
{members && members.length === 0 ? null : (
<>
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
@ -234,7 +233,7 @@ const WorkspaceInvite: NextPage = () => {
Active
</span>
) : (
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-gray-900 rounded-full">
Pending
</span>
)}

View File

@ -13,8 +13,7 @@ import fileServices from "lib/services/file.service";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/AppLayout";
import AppLayout from "layouts/app-layout";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
@ -90,6 +89,11 @@ const WorkspaceSettings = () => {
meta={{
title: "Plane - Workspace Settings",
}}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
</Breadcrumbs>
}
>
<ConfirmWorkspaceDeletion
isOpen={isOpen}
@ -99,9 +103,6 @@ const WorkspaceSettings = () => {
data={activeWorkspace ?? null}
/>
<div className="space-y-5">
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
</Breadcrumbs>
{activeWorkspace ? (
<div className="space-y-8">
<Tab.Group>

BIN
apps/app/public/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -4,7 +4,7 @@ module.exports = {
theme: {
extend: {
colors: {
theme: "#4338ca",
theme: "#3f76ff",
primary: "#f9fafb", // gray-50
secondary: "white",
},

View File

@ -25,7 +25,8 @@ export interface IIssue {
created_at: Date;
updated_at: Date;
name: string;
description: string;
// TODO change type of description
description: any;
priority: string | null;
start_date: string | null;
target_date: string | null;

View File

@ -30,19 +30,6 @@ export interface CycleIssueResponse {
cycle: string;
}
export type CycleViewProps = {
cycle: ICycle;
selectSprint: React.Dispatch<React.SetStateAction<SelectSprintType>>;
projectId: string;
workspaceSlug: string;
openIssueModal: (
sprintId: string,
issue?: IIssue,
actionType?: "create" | "edit" | "delete"
) => void;
addIssueToSprint: (sprintId: string, issueId: string) => void;
};
export type SelectSprintType =
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
| undefined;

View File

@ -36,8 +36,3 @@ export interface IWorkspaceMember {
created_by: string;
updated_by: string;
}
export interface ILastActiveWorkspaceDetails {
workspace_details: IWorkspace;
project_details: IProject[];
}

View File

@ -11,14 +11,12 @@ const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ children }: BreadcrumbsProps)
return (
<>
<div className="flex gap-3 ml-1">
<div className="flex items-center">
<div
className="bg-indigo-50 hover:bg-indigo-100 duration-300 px-3 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center grid place-items-center cursor-pointer"
className="border hover:bg-gray-100 rounded h-8 w-8 text-sm grid place-items-center text-center cursor-pointer"
onClick={() => router.back()}
>
<p className="skew-x-[20deg]">
<ArrowLeftIcon className="h-3 w-3" />
</p>
<ArrowLeftIcon className="h-3 w-3" />
</div>
{children}
</div>
@ -37,16 +35,16 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
<>
{link ? (
<Link href={link}>
<a className="bg-indigo-50 hover:bg-indigo-100 duration-300 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center">
<p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}>
<a className="text-sm border-r-2 border-gray-300 px-3">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon ?? null}
{title}
</p>
</a>
</Link>
) : (
<div className="bg-indigo-50 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center">
<p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}>
<div className="text-sm px-3">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon}
{title}
</p>

View File

@ -5,7 +5,7 @@ type Props = {
children: React.ReactNode;
type?: "button" | "submit" | "reset";
className?: string;
theme?: "primary" | "secondary" | "danger";
theme?: "primary" | "secondary" | "success" | "danger";
size?: "sm" | "rg" | "md" | "lg";
disabled?: boolean;
};
@ -37,12 +37,16 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(
theme === "primary"
? `${
disabled ? "opacity-70" : ""
} text-white shadow-sm bg-theme hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 border border-transparent`
} text-white shadow-sm bg-theme hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 border border-transparent`
: theme === "secondary"
? "border border-gray-300 bg-white"
? "border bg-white"
: theme === "success"
? `${
disabled ? "opacity-70" : ""
} text-white shadow-sm bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 border border-transparent`
: `${
disabled ? "opacity-70" : ""
} text-white shadow-sm bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 border border-transparent`,
} text-white shadow-sm bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 border border-transparent`,
size === "sm"
? "p-2 text-xs"
: size === "md"

View File

@ -131,7 +131,7 @@ const CustomListbox: React.FC<Props> = ({
? value.includes(option.value)
: value === option.value)
? "text-white"
: "text-indigo-600"
: "text-theme"
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />

View File

@ -31,7 +31,7 @@ const EmptySpace: React.FC<EmptySpaceProps> = ({ title, description, children, I
{link ? (
<div className="mt-6 flex">
<Link href={link.href}>
<a className="text-sm font-medium text-indigo-600 hover:text-indigo-500">
<a className="text-sm font-medium text-theme hover:text-indigo-500">
{link.text}
<span aria-hidden="true"> &rarr;</span>
</a>

View File

@ -24,7 +24,7 @@ const HeaderButton = ({
<>
<button
type="button"
className={`bg-theme text-white border border-indigo-600 text-xs flex items-center gap-x-1 p-2 rounded-md font-medium whitespace-nowrap outline-none ${
className={`border hover:bg-gray-100 text-gray-600 hover:text-gray-900 text-xs flex items-center gap-x-1 p-2 rounded-md font-medium whitespace-nowrap outline-none ${
position === "reverse" && "flex-row-reverse"
} ${className}`}
disabled={disabled}

View File

@ -57,7 +57,7 @@ const TextArea: React.FC<Props> = ({
"w-full outline-none px-3 py-2 bg-transparent",
mode === "primary" ? "border border-gray-300 rounded-md" : "",
mode === "transparent"
? "bg-transparent border-none transition-all ring-0 focus:ring-1 focus:ring-indigo-600 rounded"
? "bg-transparent border-none transition-all ring-0 focus:ring-1 focus:ring-theme rounded"
: "",
error ? "border-red-500" : "",
error && mode === "primary" ? "bg-red-100" : "",

View File

@ -8,5 +8,5 @@ export interface Props extends React.ComponentPropsWithoutRef<"textarea"> {
register?: UseFormRegister<any>;
mode?: "primary" | "transparent" | "secondary" | "disabled";
validations?: RegisterOptions;
error?: FieldError;
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>>;
}