Merge branch 'stage-release' of github.com:makeplane/plane into build/merge_frontend_backend

This commit is contained in:
pablohashescobar 2022-12-13 23:11:37 +05:30
commit 4d242eabf1
64 changed files with 4964 additions and 2108 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -130,15 +130,6 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
const projectName = watch("name") ?? ""; const projectName = watch("name") ?? "";
const projectIdentifier = watch("identifier") ?? ""; 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(() => { useEffect(() => {
if (projectName && isChangeIdentifierRequired) { if (projectName && isChangeIdentifierRequired) {
setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3)); 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),
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(() => { useEffect(() => {
return () => setIsChangeIdentifierRequired(true); return () => setIsChangeIdentifierRequired(true);
}, [isOpen]); }, [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 ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" onClose={handleClose}> <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(); reset();
}; };
const { handleSubmit, reset, control } = useForm<FormInput>({ const {
handleSubmit,
reset,
control,
formState: { isSubmitting },
} = useForm<FormInput>({
defaultValues: { defaultValues: {
issue_ids: [], issue_ids: [],
}, },
@ -68,6 +73,7 @@ const CycleIssuesListModal: React.FC<Props> = ({
.bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data) .bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data)
.then((res) => { .then((res) => {
console.log(res); console.log(res);
handleClose();
}) })
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
@ -138,36 +144,39 @@ const CycleIssuesListModal: React.FC<Props> = ({
</h2> </h2>
)} )}
<ul className="text-sm text-gray-700"> <ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => ( {filteredIssues.map((issue) => {
<Combobox.Option // if (issue.cycle !== cycleId)
key={issue.id} return (
as="label" <Combobox.Option
htmlFor={`issue-${issue.id}`} key={issue.id}
value={issue.id} as="label"
className={({ active }) => htmlFor={`issue-${issue.id}`}
classNames( value={issue.id}
"flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2", className={({ active }) =>
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" 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 /> {({ selected }) => (
<span <>
className={`h-1.5 w-1.5 block rounded-full`} <input type="checkbox" checked={selected} readOnly />
style={{ <span
backgroundColor: issue.state_detail.color, className="flex-shrink-0 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> <span className="flex-shrink-0 text-xs text-gray-500">
{issue.name} {activeProject?.identifier}-{issue.sequence_id}
</> </span>
)} {issue.name}
</Combobox.Option> </>
))} )}
</Combobox.Option>
);
})}
</ul> </ul>
</li> </li>
)} )}
@ -191,8 +200,13 @@ const CycleIssuesListModal: React.FC<Props> = ({
<Button type="button" theme="danger" size="sm" onClick={handleClose}> <Button type="button" theme="danger" size="sm" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button type="button" size="sm" onClick={handleSubmit(handleAddToCycle)}> <Button
Add to Cycle type="button"
size="sm"
onClick={handleSubmit(handleAddToCycle)}
disabled={isSubmitting}
>
{isSubmitting ? "Adding..." : "Add to Cycle"}
</Button> </Button>
</div> </div>
</form> </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 // fetching keys
import { STATE_LIST } from "constants/fetch-keys"; import { STATE_LIST } from "constants/fetch-keys";
// components // 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 StrictModeDroppable from "components/dnd/StrictModeDroppable";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
// ui // ui
import { Spinner } from "ui"; import { Spinner } from "ui";
// types // types
import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types"; import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types";
import ConfirmIssueDeletion from "../ConfirmIssueDeletion"; import ConfirmIssueDeletion from "../confirm-issue-deletion";
import { TrashIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
properties: Properties; properties: Properties;
@ -31,9 +30,18 @@ type Props = {
[key: string]: IIssue[]; [key: string]: IIssue[];
}; };
members: IProjectMember[] | undefined; 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 [isOpen, setIsOpen] = useState(false);
const [isIssueOpen, setIsIssueOpen] = 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 ? states?.find((s) => s.name === singleGroup)?.color
: undefined : undefined
} }
handleDeleteIssue={handleDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
/> />
))} ))}
</div> </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 }) => className={({ active }) =>
`${ `${
active ? "text-white bg-theme" : "text-gray-900" 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} value={label.id}
> >
{({ selected, active }) => ( {({ selected, active }) => (
<> <>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: label.colour }}
></span>
<span <span
className={`${ className={`${
selected || (value ?? []).some((i) => i === label.id) selected || (value ?? []).some((i) => i === label.id)

View File

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

View File

@ -4,23 +4,37 @@ import React, { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
// swr // swr
import useSWR, { mutate } from "swr"; import useSWR from "swr";
// headless ui
import { Disclosure, Listbox, Menu, Transition } from "@headlessui/react";
// ui // ui
import { Listbox, Transition } from "@headlessui/react"; import { Spinner } from "ui";
// icons // 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 // types
import { IIssue, IssueResponse, NestedKeyOf, Properties } from "types"; import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
// services
import workspaceService from "lib/services/workspace.service";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// fetch keys // fetch keys
import { PRIORITIES } from "constants/"; import { PRIORITIES } from "constants/";
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
// services
import issuesServices from "lib/services/issues.service";
import workspaceService from "lib/services/workspace.service";
// constants // constants
import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common"; import {
addSpaceIfCamelCase,
classNames,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
// types // types
type Props = { type Props = {
@ -29,6 +43,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null; selectedGroup: NestedKeyOf<IIssue> | null;
setSelectedIssue: any; setSelectedIssue: any;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>; handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
}; };
const ListView: React.FC<Props> = ({ const ListView: React.FC<Props> = ({
@ -37,377 +52,515 @@ const ListView: React.FC<Props> = ({
selectedGroup, selectedGroup,
setSelectedIssue, setSelectedIssue,
handleDeleteIssue, 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 { activeWorkspace, activeProject, states } = useUser();
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => { const { data: people } = useSWR<IWorkspaceMember[]>(
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(
activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
); );
return ( return (
<div className="mt-4 flex flex-col space-y-5"> <>
{Object.keys(groupedByIssues).map((singleGroup) => ( <CreateUpdateIssuesModal
<div key={singleGroup} className="overflow-x-auto"> isOpen={isCreateIssuesModalOpen && preloadedData?.actionType === "createIssue"}
<div className="inline-block min-w-full p-0.5 align-middle"> setIsOpen={setIsCreateIssuesModalOpen}
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"> prePopulateData={{
<table className="min-w-full"> ...preloadedData,
{selectedGroup !== null ? ( }}
<thead className="bg-gray-100"> projectId={activeProject?.id as string}
<tr> />
<th <div className="flex flex-col space-y-5">
colSpan={14} {Object.keys(groupedByIssues).map((singleGroup) => (
scope="col" <Disclosure key={singleGroup} as="div" defaultOpen>
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900" {({ open }) => (
> <div className="bg-white rounded-lg">
<div className="flex items-center gap-2"> <div className="bg-gray-100 px-4 py-3 rounded-t-lg">
{selectedGroup === "state_detail.name" ? ( <Disclosure.Button>
<span <div className="flex items-center gap-x-2">
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full" <span>
style={{ <ChevronDownIcon
backgroundColor: states?.find((s) => s.name === singleGroup)?.color, className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
}} />
></span> </span>
) : null} {selectedGroup !== null ? (
<h2 className="font-medium leading-5 capitalize">
{singleGroup === null || singleGroup === "null" {singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority" ? selectedGroup === "priority" && "No priority"
: addSpaceIfCamelCase(singleGroup)} : addSpaceIfCamelCase(singleGroup)}
<span className="ml-2 text-gray-500 font-normal text-sm"> </h2>
{groupedByIssues[singleGroup as keyof IIssue].length} ) : (
</span> <h2 className="font-medium leading-5">All Issues</h2>
</div> )}
</th> <p className="text-gray-500 text-sm">
</tr> {groupedByIssues[singleGroup as keyof IIssue].length}
</thead> </p>
) : ( </div>
<thead className="bg-gray-100"> </Disclosure.Button>
<tr> </div>
<th <Transition
colSpan={14} show={open}
scope="col" enter="transition duration-100 ease-out"
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900" enterFrom="transform opacity-0"
> enterTo="transform opacity-100"
ALL ISSUES leave="transition duration-75 ease-out"
<span className="ml-2 text-gray-500 font-normal text-sm"> leaveFrom="transform opacity-100"
{groupedByIssues[singleGroup as keyof IIssue].length} leaveTo="transform opacity-0"
</span> >
</th> <Disclosure.Panel>
</tr> <div className="divide-y-2">
</thead> {groupedByIssues[singleGroup] ? (
)} groupedByIssues[singleGroup].length > 0 ? (
<tbody className="bg-white"> groupedByIssues[singleGroup].map((issue: IIssue) => {
{groupedByIssues[singleGroup].length > 0 const assignees = [
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => { ...(issue?.assignees_list ?? []),
const assignees = [ ...(issue?.assignees ?? []),
...(issue?.assignees_list ?? []), ]?.map((assignee) => {
...(issue?.assignees ?? []), const tempPerson = people?.find(
]?.map( (p) => p.member.id === assignee
(assignee) => people?.find((p) => p.member.id === assignee)?.member.email )?.member;
);
return ( return {
<tr avatar: tempPerson?.avatar,
key={issue.id} first_name: tempPerson?.first_name,
className={classNames( email: tempPerson?.email,
index === 0 ? "border-gray-300" : "border-gray-200", };
"border-t" });
)}
> return (
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]"> <div
<Link href={`/projects/${issue.project}/issues/${issue.id}`}> key={issue.id}
<a className="hover:text-theme duration-300">{issue.name}</a> className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
</Link> >
</td> <div className="flex items-center gap-2">
{Object.keys(properties).map( <span
(key) => className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
properties[key as keyof Properties] && ( style={{
<React.Fragment key={key}> backgroundColor: issue.state_detail.color,
{(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} <Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
</td> <a className="group relative flex items-center gap-2">
) : (key as keyof Properties) === "priority" ? ( {properties.key && (
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative"> <span className="flex-shrink-0 text-xs text-gray-500">
<Listbox {activeProject?.identifier}-{issue.sequence_id}
as="div" </span>
value={issue.priority} )}
onChange={(data: string) => { <span className="">{issue.name}</span>
partialUpdateIssue({ priority: data }, issue.id); <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>
className="flex-shrink-0" <div>{issue.name}</div>
> </div>
{({ open }) => ( </a>
<> </Link>
<div className=""> </div>
<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"> <div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
<span {properties.priority && (
className={classNames( <Listbox
issue.priority ? "" : "text-gray-900", as="div"
"hidden truncate capitalize sm:block w-16" 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"} {priority}
</span> </Listbox.Option>
</Listbox.Button> ))}
</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 <Transition
show={open} show={open}
as={React.Fragment} as={React.Fragment}
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" 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"> <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) => ( {states?.map((state) => (
<Listbox.Option <Listbox.Option
key={priority} key={state.id}
className={({ active }) => className={({ active }) =>
classNames( classNames(
active ? "bg-indigo-50" : "bg-white", active ? "bg-indigo-50" : "bg-white",
"cursor-pointer capitalize select-none px-3 py-2" "cursor-pointer select-none px-3 py-2"
) )
} }
value={priority} value={state.id}
> >
{priority} {addSpaceIfCamelCase(state.name)}
</Listbox.Option> </Listbox.Option>
))} ))}
</Listbox.Options> </Listbox.Options>
</Transition> </Transition>
</div> </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>
</Listbox> <div>{issue.state_detail.name}</div>
</td> </div>
) : (key as keyof Properties) === "assignee" ? ( </>
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative"> )}
<Listbox </Listbox>
as="div" )}
value={issue.assignees} {properties.start_date && (
onChange={(data: any) => { <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">
const newData = issue.assignees ?? []; <CalendarDaysIcon className="h-4 w-4" />
if (newData.includes(data)) { {issue.start_date
newData.splice(newData.indexOf(data), 1); ? renderShortNumericDateFormat(issue.start_date)
} else { : "N/A"}
newData.push(data); <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>
partialUpdateIssue( <div>
{ assignees_list: newData }, {renderShortNumericDateFormat(issue.start_date ?? "")}
issue.id </div>
); </div>
}} </div>
className="flex-shrink-0" )}
> {properties.target_date && (
{({ open }) => ( <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 ${
<div> issue.target_date === null
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border"> ? ""
{() => { : issue.target_date < new Date().toISOString()
if (assignees.length > 0) ? "text-red-600"
return ( : findHowManyDaysLeft(issue.target_date) <= 3 &&
<> "text-orange-400"
{assignees.map((assignee, index) => ( }`}
<div >
key={index} <CalendarDaysIcon className="h-4 w-4" />
className={ {issue.target_date
"hidden truncate sm:block text-left" ? 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">
{assignee} <h5 className="font-medium mb-1 text-gray-900">
</div> Target date
))} </h5>
</> <div>
); {renderShortNumericDateFormat(issue.target_date ?? "")}
else return <span>None</span>; </div>
}} <div>
</Listbox.Button> {issue.target_date &&
(issue.target_date < new Date().toISOString()
<Transition ? `Target date has passed by ${findHowManyDaysLeft(
show={open} issue.target_date
as={React.Fragment} )} days`
leave="transition ease-in duration-100" : findHowManyDaysLeft(issue.target_date) <= 3
leaveFrom="opacity-100" ? `Target date is in ${findHowManyDaysLeft(
leaveTo="opacity-0" issue.target_date
> )} days`
<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"> : "Target date")}
{people?.map((person) => ( </div>
<Listbox.Option </div>
key={person.id} </div>
className={({ active }) => )}
classNames( {properties.assignee && (
active ? "bg-indigo-50" : "bg-white", <Listbox
"cursor-pointer select-none px-3 py-2" as="div"
) value={issue.assignees}
} onChange={(data: any) => {
value={person.member.id} const newData = issue.assignees ?? [];
> if (newData.includes(data)) {
<div newData.splice(newData.indexOf(data), 1);
className={`flex items-center gap-x-1 ${ } else {
assignees.includes( newData.push(data);
person.member.first_name }
) partialUpdateIssue({ assignees_list: newData }, issue.id);
? "font-medium" }}
: "font-normal" className="group relative flex-shrink-0"
}`} >
> {({ open }) => (
{person.member.avatar && <>
person.member.avatar !== "" ? ( <div>
<div className="relative w-4 h-4"> <Listbox.Button>
<Image <div className="flex items-center gap-1 text-xs cursor-pointer">
src={person.member.avatar} {assignees.length > 0 ? (
alt="avatar" assignees.map((assignee, index: number) => (
className="rounded-full" <div
layout="fill" key={index}
objectFit="cover" className={`relative z-[1] h-5 w-5 rounded-full ${
/> index !== 0 ? "-ml-2.5" : ""
</div> }`}
) : ( >
<p> {assignee.avatar && assignee.avatar !== "" ? (
{person.member.first_name.charAt(0)} <div className="h-5 w-5 border-2 bg-white border-white rounded-full">
</p> <Image
)} src={assignee.avatar}
<p>{person.member.first_name}</p> height="100%"
width="100%"
className="rounded-full"
alt={assignee?.first_name}
/>
</div> </div>
</Listbox.Option> ) : (
))} <div
</Listbox.Options> className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
</Transition> >
{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> </div>
</> </Listbox.Button>
)}
</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>
<Transition <Transition
show={open} show={open}
as={React.Fragment} as={React.Fragment}
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" 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"> <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">
{states?.map((state) => ( {people?.map((person) => (
<Listbox.Option <Listbox.Option
key={state.id} key={person.id}
className={({ active }) => className={({ active }) =>
classNames( classNames(
active ? "bg-indigo-50" : "bg-white", active ? "bg-indigo-50" : "bg-white",
"cursor-pointer select-none px-3 py-2" "cursor-pointer select-none p-2"
) )
} }
value={state.id} value={person.member.id}
> >
{addSpaceIfCamelCase(state.name)} <div
</Listbox.Option> className={`flex items-center gap-x-1 ${
))} assignees.includes({
</Listbox.Options> avatar: person.member.avatar,
</Transition> first_name: person.member.first_name,
</div> email: person.member.email,
</> })
)} ? "font-medium"
</Listbox> : "font-normal"
</td> }`}
) : (key as keyof Properties) === "target_date" ? ( >
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap"> {person.member.avatar &&
{issue.target_date person.member.avatar !== "" ? (
? renderShortNumericDateFormat(issue.target_date) <div className="relative h-4 w-4">
: "-"} <Image
</td> src={person.member.avatar}
) : ( alt="avatar"
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize"> className="rounded-full"
{issue[key as keyof IIssue] ?? layout="fill"
(issue[key as keyof IIssue] as any)?.name ?? objectFit="cover"
"None"} />
</td> </div>
)} ) : (
</React.Fragment> <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 !== ""
<td className="px-3"> ? person.member.first_name.charAt(0)
<div className="flex justify-end items-center gap-2"> : person.member.email.charAt(0)}
<button </div>
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" <p>
onClick={() => { {person.member.first_name &&
setSelectedIssue({ person.member.first_name !== ""
...issue, ? person.member.first_name
actionType: "edit", : person.member.email}
}); </p>
}} </div>
> </Listbox.Option>
<PencilIcon className="h-3 w-3" /> ))}
</button> </Listbox.Options>
<button </Transition>
type="button" </div>
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" <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">
onClick={() => { <h5 className="font-medium mb-1">Assigned to</h5>
handleDeleteIssue(issue.id); <div>
}} {issue.assignee_details?.length > 0
> ? issue.assignee_details
<TrashIcon className="h-3 w-3" /> .map((assignee) => assignee.first_name)
</button> .join(", ")
: "No one"}
</div>
</div>
</>
)}
</Listbox>
)}
<Menu as="div" className="relative">
<Menu.Button
as="button"
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
>
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<Menu.Item>
<button
type="button"
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
onClick={() => {
setSelectedIssue({
...issue,
actionType: "edit",
});
}}
>
Edit
</button>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
type="button"
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
onClick={() => {
handleDeleteIssue(issue.id);
}}
>
Delete permanently
</button>
</div>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
</div> </div>
</td> );
</tr> })
); ) : (
}) <p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
: null} )
</tbody> ) : (
</table> <div className="h-full w-full flex items-center justify-center">
</div> <Spinner />
</div> </div>
</div> )}
))} </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 // swr
import { mutate } from "swr"; import { mutate } from "swr";
// headless ui // headless ui
@ -26,7 +26,7 @@ type Props = {
const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) => { const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { activeWorkspace } = useUser(); const { activeWorkspace, activeProject } = useUser();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -70,7 +70,7 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <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 <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" 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"> <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="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <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"> <div>
<ExclamationTriangleIcon <div className="mx-auto h-16 w-16 grid place-items-center rounded-full bg-red-100">
className="h-6 w-6 text-red-600" <ExclamationTriangleIcon
aria-hidden="true" className="h-8 w-8 text-red-600"
/> aria-hidden="true"
</div> />
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> </div>
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> <Dialog.Title
Delete Issue 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> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Are you sure you want to delete issue - {`"`} All of the data related to the issue will be permanently removed. This
<span className="italic">{data?.name}</span> action cannot be undone.
{`"`} ? All of the data related to the issue will be permanently removed.
This action cannot be undone.
</p> </p>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -71,42 +71,6 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
return ( return (
<div className="space-y-5"> <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 ? (
comments.length > 0 ? ( comments.length > 0 ? (
<div className="space-y-5"> <div className="space-y-5">
@ -127,6 +91,37 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
<Spinner /> <Spinner />
</div> </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> </div>
); );
}; };

View File

@ -68,7 +68,7 @@ const ProjectMemberInvitations: React.FC<Props> = ({
{!isMember ? ( {!isMember ? (
<input <input
id={project.id} 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" aria-describedby="workspaces"
name={project.id} name={project.id}
checked={invitationsRespond.includes(project.id)} checked={invitationsRespond.includes(project.id)}

View File

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

View File

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

View File

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

@ -6,13 +6,21 @@ import { useRouter } from "next/router";
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// layouts // layouts
import Container from "layouts/Container"; import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/Sidebar"; import Sidebar from "layouts/Navbar/main-sidebar";
import Header from "layouts/Navbar/Header";
// components // components
import CreateProjectModal from "components/project/create-project-modal"; import CreateProjectModal from "components/project/create-project-modal";
// types // types
import type { Props } from "./types"; import type { Props } from "./types";
const AppLayout: React.FC<Props> = ({ meta, children, noPadding = false, bg = "primary" }) => { const AppLayout: React.FC<Props> = ({
meta,
children,
noPadding = false,
bg = "primary",
breadcrumbs,
right,
}) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const router = useRouter(); const router = useRouter();
@ -28,12 +36,15 @@ const AppLayout: React.FC<Props> = ({ meta, children, noPadding = false, bg = "p
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} /> <CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="h-screen w-full flex overflow-x-hidden"> <div className="h-screen w-full flex overflow-x-hidden">
<Sidebar /> <Sidebar />
<main <main className="h-screen w-full flex flex-col overflow-y-auto min-w-0">
className={`h-full w-full min-w-0 overflow-y-auto ${noPadding ? "" : "p-5"} ${ <Header breadcrumbs={breadcrumbs} right={right} />
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary" <div
}`} className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
> bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
{children} }`}
>
{children}
</div>
</main> </main>
</div> </div>
</Container> </Container>

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; children: React.ReactNode;
noPadding?: boolean; noPadding?: boolean;
bg?: "primary" | "secondary"; bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element;
right?: JSX.Element;
}; };

View File

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

View File

@ -19,12 +19,7 @@ import APIService from "lib/services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
// types // types
import { import { IWorkspace, IWorkspaceMember, IWorkspaceMemberInvitation } from "types";
ILastActiveWorkspaceDetails,
IWorkspace,
IWorkspaceMember,
IWorkspaceMemberInvitation,
} from "types";
class WorkspaceService extends APIService { class WorkspaceService extends APIService {
constructor() { 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[]> { async userWorkspaceInvitations(): Promise<IWorkspaceMemberInvitation[]> {
return this.get(USER_WORKSPACE_INVITATIONS) return this.get(USER_WORKSPACE_INVITATIONS)
.then((response) => { .then((response) => {

View File

@ -68,7 +68,7 @@ const CreateWorkspace: NextPage = () => {
<DefaultLayout> <DefaultLayout>
<div className="flex flex-col items-center justify-center w-full h-full px-4"> <div className="flex flex-col items-center justify-center w-full h-full px-4">
{user && ( {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> <p className="text-sm text-center">logged in as {user.email}</p>
</div> </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"> <div className="flex min-h-full flex-col items-center justify-center p-4 sm:p-0">
{user && ( {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> <p className="text-sm text-center">logged in as {user.email}</p>
</div> </div>
)} )}
@ -120,7 +120,7 @@ const OnBoard: NextPage = () => {
); );
}} }}
type="checkbox" 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"> <label htmlFor={item.id} className="text-sm">
Accept Accept

View File

@ -5,7 +5,7 @@ import type { NextPage } from "next";
// swr // swr
import useSWR from "swr"; import useSWR from "swr";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// ui // ui
@ -71,176 +71,107 @@ const MyIssues: NextPage = () => {
}); });
}; };
const handleWorkspaceChange = (workspaceId: string | null) => {
setSelectedWorkspace(workspaceId);
};
return ( 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"> <div className="w-full h-full flex flex-col space-y-5">
{myIssues ? ( {myIssues ? (
<> <>
{myIssues.length > 0 ? ( {myIssues.length > 0 ? (
<> <div className="flex flex-col">
<Breadcrumbs> <div className="overflow-x-auto ">
<BreadcrumbItem title="My Issues" /> <div className="inline-block min-w-full align-middle px-0.5 py-2">
</Breadcrumbs> <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<div className="flex items-center justify-between cursor-pointer w-full"> <table className="min-w-full">
<h2 className="text-2xl font-medium">My Issues</h2> <thead className="bg-gray-100">
<div className="flex items-center gap-x-3"> <tr className="text-left">
<Menu as="div" className="relative inline-block w-40"> <th
<div className="w-full"> scope="col"
<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"> className="px-3 py-3.5 text-sm font-semibold text-gray-900"
<span className="flex gap-x-1 items-center"> >
{workspaces?.find((w) => w.id === selectedWorkspace)?.name ?? NAME
"All workspaces"} </th>
</span> <th
<div className="flex-grow flex justify-end"> scope="col"
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" /> className="px-3 py-3.5 text-sm font-semibold text-gray-900"
</div> >
</Menu.Button> DESCRIPTION
</div> </th>
<th
<Transition scope="col"
as={React.Fragment} className="px-3 py-3.5 text-sm font-semibold text-gray-900"
enter="transition ease-out duration-100" >
enterFrom="transform opacity-0 scale-95" PROJECT
enterTo="transform opacity-100 scale-100" </th>
leave="transition ease-in duration-75" <th
leaveFrom="transform opacity-100 scale-100" scope="col"
leaveTo="transform opacity-0 scale-95" className="px-3 py-3.5 text-sm font-semibold text-gray-900"
> >
<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"> PRIORITY
<div className="p-1"> </th>
<Menu.Item> <th
{({ active }) => ( scope="col"
<button className="px-3 py-3.5 text-sm font-semibold text-gray-900"
type="button" >
className={`${ STATUS
active ? "bg-theme text-white" : "text-gray-900" </th>
} group flex w-full items-center rounded-md p-2 text-xs`} </tr>
onClick={() => handleWorkspaceChange(null)} </thead>
> <tbody className="bg-white">
All workspaces {myIssues.map((myIssue, index) => (
</button> <tr
key={myIssue.id}
className={classNames(
index === 0 ? "border-gray-300" : "border-gray-200",
"border-t text-sm text-gray-900"
)} )}
</Menu.Item> >
{workspaces && <td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
workspaces.map((workspace) => ( <Link href={`/projects/${myIssue.project}/issues/${myIssue.id}`}>
<Menu.Item key={workspace.id}> <a>{myIssue.name}</a>
{({ active }) => ( </Link>
<button </td>
type="button" <td className="px-3 py-4 max-w-[15rem] truncate">
className={`${ {/* {myIssue.description} */}
active ? "bg-theme text-white" : "text-gray-900" </td>
} group flex w-full items-center rounded-md p-2 text-xs`} <td className="px-3 py-4">
onClick={() => handleWorkspaceChange(workspace.id)} {myIssue.project_detail?.name}
> <br />
{workspace.name} <span className="text-xs">{`(${myIssue.project_detail?.identifier}-${myIssue.sequence_id})`}</span>
</button> </td>
)} <td className="px-3 py-4 capitalize">{myIssue.priority}</td>
</Menu.Item> <td className="relative px-3 py-4">
))} <ChangeStateDropdown
</div> issue={myIssue}
</Menu.Items> updateIssues={updateMyIssues}
</Transition> />
</Menu> </td>
<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>
</tr> </tr>
</thead> ))}
<tbody className="bg-white"> </tbody>
{myIssues </table>
.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>
</div> </div>
</div> </div>
</div> </div>
</> </div>
) : ( ) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4"> <div className="w-full h-full flex flex-col justify-center items-center px-4">
<EmptySpace <EmptySpace

View File

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

View File

@ -10,7 +10,7 @@ import useUser from "lib/hooks/useUser";
// hoc // hoc
import withAuthWrapper from "lib/hoc/withAuthWrapper"; import withAuthWrapper from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// ui // ui
import { Button } from "ui"; import { Button } from "ui";
// swr // swr
@ -89,7 +89,7 @@ const MyWorkspacesInvites: NextPage = () => {
) )
} }
type="checkbox" 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>
<div className="ml-3 text-sm flex justify-between w-full"> <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"; import React, { useEffect, useState } from "react";
// next // next
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import type { NextPage } from "next"; import type { NextPage } from "next";
import Link from "next/link";
// swr // swr
import useSWR, { mutate } from "swr"; import useSWR from "swr";
// services // services
import issuesServices from "lib/services/issues.service"; import issuesServices from "lib/services/issues.service";
import sprintService from "lib/services/cycles.service"; import sprintService from "lib/services/cycles.service";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// fetching keys // fetching keys
import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys"; import { CYCLE_LIST } from "constants/fetch-keys";
// hoc // hoc
import withAuth from "lib/hoc/withAuthWrapper"; import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// components // components
import CycleView from "components/project/cycles/CycleView"; import CycleIssuesListModal from "components/project/cycles/CycleIssuesListModal";
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion"; import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion"; import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal"; import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// ui // ui
import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySpaceItem } from "ui"; import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySpaceItem } from "ui";
// icons // icons
import { PlusIcon } from "@heroicons/react/20/solid"; import { ArrowPathIcon, ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssue, ICycle, SelectSprintType, SelectIssue, CycleIssueResponse } from "types"; import { IIssue, ICycle, SelectSprintType, SelectIssue, Properties } from "types";
import { DragDropContext, DropResult } from "react-beautiful-dnd"; // constants
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
const ProjectSprints: NextPage = () => { const ProjectSprints: NextPage = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -37,6 +42,8 @@ const ProjectSprints: NextPage = () => {
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false); const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>(); const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(); const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const [cycleId, setCycleId] = useState("");
const { activeWorkspace, activeProject, issues } = useUser(); const { activeWorkspace, activeProject, issues } = useUser();
@ -45,13 +52,18 @@ const ProjectSprints: NextPage = () => {
const { projectId } = router.query; const { projectId } = router.query;
const { data: cycles } = useSWR<ICycle[]>( const { data: cycles } = useSWR<ICycle[]>(
projectId && activeWorkspace ? CYCLE_LIST(projectId as string) : null, activeWorkspace && projectId ? CYCLE_LIST(projectId as string) : null,
activeWorkspace && projectId activeWorkspace && projectId
? () => sprintService.getCycles(activeWorkspace.slug, projectId as string) ? () => sprintService.getCycles(activeWorkspace.slug, projectId as string)
: null : null
); );
const openIssueModal = ( const [properties, setProperties] = useIssuesProperties(
activeWorkspace?.slug,
projectId as string
);
const openCreateIssueModal = (
cycleId: string, cycleId: string,
issue?: IIssue, issue?: IIssue,
actionType: "create" | "edit" | "delete" = "create" actionType: "create" | "edit" | "delete" = "create"
@ -67,89 +79,9 @@ const ProjectSprints: NextPage = () => {
} }
}; };
const addIssueToSprint = (cycleId: string, issueId: string) => { const openIssuesListModal = (cycleId: string) => {
if (!activeWorkspace || !projectId) return; setCycleId(cycleId);
setCycleIssuesListModal(true);
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);
}; };
useEffect(() => { useEffect(() => {
@ -171,6 +103,66 @@ const ProjectSprints: NextPage = () => {
meta={{ meta={{
title: "Plane - Cycles", 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 <CreateUpdateSprintsModal
isOpen={ isOpen={
@ -203,32 +195,20 @@ const ProjectSprints: NextPage = () => {
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
projectId={projectId as string} projectId={projectId as string}
/> />
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycleId}
/>
{cycles ? ( {cycles ? (
cycles.length > 0 ? ( cycles.length > 0 ? (
<div className="h-full w-full space-y-5"> <div className="space-y-5">
<Breadcrumbs> {cycles.map((cycle) => (
<BreadcrumbItem title="Projects" link="/projects" /> <Link key={cycle.id} href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} /> <a className="block bg-white p-3 rounded-md">{cycle.name}</a>
</Breadcrumbs> </Link>
<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> </div>
) : ( ) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4"> <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 // swr
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react hook form // react hook form
import { Controller, useForm } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
// headless ui // headless ui
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react"; import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
// services // services
@ -17,14 +17,13 @@ import {
PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUES_ACTIVITY,
PROJECT_ISSUES_COMMENTS, PROJECT_ISSUES_COMMENTS,
PROJECT_ISSUES_LIST, PROJECT_ISSUES_LIST,
STATE_LIST,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// hoc // hoc
import withAuth from "lib/hoc/withAuthWrapper"; import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// components // components
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection"; import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
@ -49,13 +48,14 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import Link from "next/link"; import Link from "next/link";
import AddAsSubIssue from "components/command-palette/addAsSubIssue"; 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 IssueDetail: NextPage = () => {
const router = useRouter(); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { issueId, projectId } = router.query;
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false); const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
@ -67,19 +67,18 @@ const IssueDetail: NextPage = () => {
>(undefined); >(undefined);
const [issueDescriptionValue, setIssueDescriptionValue] = useState(""); const [issueDescriptionValue, setIssueDescriptionValue] = useState("");
const router = useRouter();
const { issueId, projectId } = router.query;
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
const handleDescriptionChange: any = (value: any) => { const handleDescriptionChange: any = (value: any) => {
console.log(value); console.log(value);
setIssueDescriptionValue(value); setIssueDescriptionValue(value);
}; };
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
ssr: false,
});
const LexicalViewer = dynamic(() => import("components/lexical/viewer"), {
ssr: false,
});
const { const {
register, register,
formState: { errors }, formState: { errors },
@ -143,8 +142,13 @@ const IssueDetail: NextPage = () => {
false false
); );
const payload = {
...formData,
// description: formData.description ? JSON.parse(formData.description) : null,
};
issuesServices issuesServices
.patchIssue(activeWorkspace.slug, projectId as string, issueId as string, formData) .patchIssue(activeWorkspace.slug, projectId as string, issueId as string, payload)
.then((response) => { .then((response) => {
console.log(response); console.log(response);
}) })
@ -159,6 +163,7 @@ const IssueDetail: NextPage = () => {
if (issueDetail) if (issueDetail)
reset({ reset({
...issueDetail, ...issueDetail,
// description: JSON.stringify(issueDetail.description),
blockers_list: blockers_list:
issueDetail.blockers_list ?? issueDetail.blockers_list ??
issueDetail.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id), issueDetail.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id),
@ -208,8 +213,50 @@ const IssueDetail: NextPage = () => {
} }
}; };
// console.log(issueDetail);
return ( 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 <CreateUpdateIssuesModal
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
@ -218,6 +265,11 @@ const IssueDetail: NextPage = () => {
...preloadedData, ...preloadedData,
}} }}
/> />
<ConfirmIssueDeletion
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetail}
/>
<AddAsSubIssue <AddAsSubIssue
isOpen={isAddAsSubIssueOpen} isOpen={isAddAsSubIssueOpen}
setIsOpen={setIsAddAsSubIssueOpen} setIsOpen={setIsAddAsSubIssueOpen}
@ -226,19 +278,7 @@ const IssueDetail: NextPage = () => {
{issueDetail && activeProject ? ( {issueDetail && activeProject ? (
<div className="flex gap-5"> <div className="flex gap-5">
<div className="basis-3/4 space-y-5 p-5"> <div className="basis-3/4 space-y-5 p-5">
<div className="mb-5"> <div className="mb-5"></div>
<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="rounded-lg"> <div className="rounded-lg">
{issueDetail.parent !== null && issueDetail.parent !== "" ? ( {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"> <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 {/* <Controller
name="description" name="description"
control={control} control={control}
render={({ field }) => ( render={({ field: { value, onChange } }) => (
<RichTextEditor <RichTextEditor
{...field} // value={JSON.stringify(issueDetail.description)}
value={value}
onChange={(val) => {
debounce(() => {
console.log("Debounce");
// handleSubmit(submitChanges)();
}, 5000)();
onChange(val);
}}
id="issueDescriptionEditor" id="issueDescriptionEditor"
value={JSON.parse(issueDetail.description)}
/> />
)} )}
/> */} /> */}
{/* <LexicalViewer id="descriptionViewer" value={JSON.parse(issueDetail.description)} /> */}
</div> </div>
<div className="mt-2"> <div className="mt-2">
{subIssues && subIssues.length > 0 ? ( {subIssues && subIssues.length > 0 ? (
@ -568,34 +614,13 @@ const IssueDetail: NextPage = () => {
</Tab.Group> </Tab.Group>
</div> </div>
</div> </div>
<div className="basis-1/4 rounded-lg space-y-5 p-5 border-l"> <div className="h-full basis-1/4 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>
<IssueDetailSidebar <IssueDetailSidebar
control={control} control={control}
issueDetail={issueDetail} issueDetail={issueDetail}
submitChanges={submitChanges} submitChanges={submitChanges}
watch={watch} watch={watch}
setDeleteIssueModal={setDeleteIssueModal}
/> />
</div> </div>
</div> </div>

View File

@ -3,11 +3,13 @@ import React, { useEffect, useState } from "react";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// swr // swr
import useSWR from "swr"; import useSWR, { mutate } from "swr";
// headless ui // headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// hoc // hoc
import withAuth from "lib/hoc/withAuthWrapper"; import withAuth from "lib/hoc/withAuthWrapper";
// services
import issuesServices from "lib/services/issues.service";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
import useIssuesProperties from "lib/hooks/useIssuesProperties"; import useIssuesProperties from "lib/hooks/useIssuesProperties";
@ -18,23 +20,31 @@ import projectService from "lib/services/project.service";
// commons // commons
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common"; import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// hooks // hooks
import useIssuesFilter from "lib/hooks/useIssuesFilter"; import useIssuesFilter from "lib/hooks/useIssuesFilter";
// components // components
import ListView from "components/project/issues/ListView"; import ListView from "components/project/issues/ListView";
import BoardView from "components/project/issues/BoardView"; 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"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
// ui // ui
import { Spinner, CustomMenu, BreadcrumbItem, Breadcrumbs } from "ui"; import {
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace"; Spinner,
import HeaderButton from "ui/HeaderButton"; CustomMenu,
BreadcrumbItem,
Breadcrumbs,
EmptySpace,
EmptySpaceItem,
HeaderButton,
} from "ui";
// icons // icons
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
// types // 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 }> = [ const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" }, { 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 { const {
issueView, issueView,
setIssueView, setIssueView,
@ -111,7 +141,7 @@ const ProjectIssues: NextPage = () => {
setFilterIssue, setFilterIssue,
orderBy, orderBy,
filterIssue, filterIssue,
} = useIssuesFilter(projectIssues); } = useIssuesFilter(projectIssues?.results ?? []);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@ -123,7 +153,161 @@ const ProjectIssues: NextPage = () => {
}, [isOpen]); }, [isOpen]);
return ( 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 <CreateUpdateIssuesModal
isOpen={isOpen && selectedIssue?.actionType !== "delete"} isOpen={isOpen && selectedIssue?.actionType !== "delete"}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
@ -141,167 +325,6 @@ const ProjectIssues: NextPage = () => {
</div> </div>
) : projectIssues.count > 0 ? ( ) : 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" ? ( {issueView === "list" ? (
<ListView <ListView
properties={properties} properties={properties}
@ -309,14 +332,17 @@ const ProjectIssues: NextPage = () => {
selectedGroup={groupByProperty} selectedGroup={groupByProperty}
setSelectedIssue={setSelectedIssue} setSelectedIssue={setSelectedIssue}
handleDeleteIssue={setDeleteIssue} handleDeleteIssue={setDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
/> />
) : ( ) : (
<div className="h-full"> <div className="h-screen">
<BoardView <BoardView
properties={properties} properties={properties}
selectedGroup={groupByProperty} selectedGroup={groupByProperty}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}
members={members} members={members}
handleDeleteIssue={setDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
/> />
</div> </div>
)} )}

View File

@ -17,7 +17,7 @@ import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
// hoc // hoc
import withAuth from "lib/hoc/withAuthWrapper"; import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// components // components
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal"; import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove"; import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove";
@ -92,7 +92,15 @@ const ProjectMembers: NextPage = () => {
]; ];
return ( 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 <ConfirmProjectMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)} isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => { onClose={() => {
@ -140,14 +148,6 @@ const ProjectMembers: NextPage = () => {
</div> </div>
) : ( ) : (
<div className="h-full w-full space-y-5"> <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 : ( {members && members.length === 0 ? null : (
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300"> <table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
@ -246,7 +246,7 @@ const ProjectMembers: NextPage = () => {
Active Active
</span> </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 Pending
</span> </span>
)} )}

View File

@ -12,7 +12,7 @@ import { Tab } from "@headlessui/react";
// hoc // hoc
import withAuth from "lib/hoc/withAuthWrapper"; import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import SettingsLayout from "layouts/settings-layout";
// service // service
import projectServices from "lib/services/project.service"; import projectServices from "lib/services/project.service";
// hooks // hooks
@ -26,6 +26,26 @@ import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
// types // types
import type { IProject, IWorkspace } from "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> = { const defaultValues: Partial<IProject> = {
name: "", name: "",
description: "", description: "",
@ -34,27 +54,6 @@ const defaultValues: Partial<IProject> = {
}; };
const ProjectSettings: NextPage = () => { 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 { const {
register, register,
handleSubmit, 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 ( return (
<AppLayout> <SettingsLayout
<div className="space-y-5 mb-5"> breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" /> <BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} /> <BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
</Breadcrumbs> </Breadcrumbs>
</div> }
links={sidebarLinks}
>
{projectDetails ? ( {projectDetails ? (
<div className="space-y-3"> <div className="space-y-3">
<Tab.Group> <Tab.Group>
@ -186,7 +209,7 @@ const ProjectSettings: NextPage = () => {
<Spinner /> <Spinner />
</div> </div>
)} )}
</AppLayout> </SettingsLayout>
); );
}; };

View File

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

View File

@ -101,7 +101,7 @@ const SignIn: NextPage = () => {
> >
{isGoogleAuthenticationLoading && ( {isGoogleAuthenticationLoading && (
<div className="absolute top-0 left-0 w-full h-full bg-white z-50 flex items-center justify-center"> <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>
)} )}
<div className="w-full h-screen flex justify-center items-center bg-gray-50 overflow-auto"> <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 // react
import React from "react"; import React from "react";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// swr // swr
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks

View File

@ -17,7 +17,7 @@ import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// hoc // hoc
import withAuthWrapper from "lib/hoc/withAuthWrapper"; import withAuthWrapper from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// components // components
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal"; import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove"; import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove";
@ -75,6 +75,12 @@ const WorkspaceInvite: NextPage = () => {
meta={{ meta={{
title: "Plane - Workspace Invite", title: "Plane - Workspace Invite",
}} }}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Members`} />
</Breadcrumbs>
}
right={<HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />}
> >
<ConfirmWorkspaceMemberRemove <ConfirmWorkspaceMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)} isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
@ -132,13 +138,6 @@ const WorkspaceInvite: NextPage = () => {
</div> </div>
) : ( ) : (
<div className="w-full space-y-5"> <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 : ( {members && members.length === 0 ? null : (
<> <>
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300"> <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 Active
</span> </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 Pending
</span> </span>
)} )}

View File

@ -13,8 +13,7 @@ import fileServices from "lib/services/file.service";
// hoc // hoc
import withAuth from "lib/hoc/withAuthWrapper"; import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/app-layout";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast"; import useToast from "lib/hooks/useToast";
@ -90,6 +89,11 @@ const WorkspaceSettings = () => {
meta={{ meta={{
title: "Plane - Workspace Settings", title: "Plane - Workspace Settings",
}} }}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
</Breadcrumbs>
}
> >
<ConfirmWorkspaceDeletion <ConfirmWorkspaceDeletion
isOpen={isOpen} isOpen={isOpen}
@ -99,9 +103,6 @@ const WorkspaceSettings = () => {
data={activeWorkspace ?? null} data={activeWorkspace ?? null}
/> />
<div className="space-y-5"> <div className="space-y-5">
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
</Breadcrumbs>
{activeWorkspace ? ( {activeWorkspace ? (
<div className="space-y-8"> <div className="space-y-8">
<Tab.Group> <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: { theme: {
extend: { extend: {
colors: { colors: {
theme: "#4338ca", theme: "#3f76ff",
primary: "#f9fafb", // gray-50 primary: "#f9fafb", // gray-50
secondary: "white", secondary: "white",
}, },

View File

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

View File

@ -30,19 +30,6 @@ export interface CycleIssueResponse {
cycle: string; 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 = export type SelectSprintType =
| (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | (ICycle & { actionType: "edit" | "delete" | "create-issue" })
| undefined; | undefined;

View File

@ -36,8 +36,3 @@ export interface IWorkspaceMember {
created_by: string; created_by: string;
updated_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 ( return (
<> <>
<div className="flex gap-3 ml-1"> <div className="flex items-center">
<div <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()} onClick={() => router.back()}
> >
<p className="skew-x-[20deg]"> <ArrowLeftIcon className="h-3 w-3" />
<ArrowLeftIcon className="h-3 w-3" />
</p>
</div> </div>
{children} {children}
</div> </div>
@ -37,16 +35,16 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
<> <>
{link ? ( {link ? (
<Link href={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"> <a className="text-sm border-r-2 border-gray-300 px-3">
<p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}> <p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon ?? null} {icon ?? null}
{title} {title}
</p> </p>
</a> </a>
</Link> </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"> <div className="text-sm px-3">
<p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}> <p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon} {icon}
{title} {title}
</p> </p>

View File

@ -5,7 +5,7 @@ type Props = {
children: React.ReactNode; children: React.ReactNode;
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
className?: string; className?: string;
theme?: "primary" | "secondary" | "danger"; theme?: "primary" | "secondary" | "success" | "danger";
size?: "sm" | "rg" | "md" | "lg"; size?: "sm" | "rg" | "md" | "lg";
disabled?: boolean; disabled?: boolean;
}; };
@ -37,12 +37,16 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(
theme === "primary" theme === "primary"
? `${ ? `${
disabled ? "opacity-70" : "" 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" : 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" : "" 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" size === "sm"
? "p-2 text-xs" ? "p-2 text-xs"
: size === "md" : size === "md"

View File

@ -131,7 +131,7 @@ const CustomListbox: React.FC<Props> = ({
? value.includes(option.value) ? value.includes(option.value)
: value === option.value) : value === option.value)
? "text-white" ? "text-white"
: "text-indigo-600" : "text-theme"
}`} }`}
> >
<CheckIcon className="h-5 w-5" aria-hidden="true" /> <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 ? ( {link ? (
<div className="mt-6 flex"> <div className="mt-6 flex">
<Link href={link.href}> <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} {link.text}
<span aria-hidden="true"> &rarr;</span> <span aria-hidden="true"> &rarr;</span>
</a> </a>

View File

@ -24,7 +24,7 @@ const HeaderButton = ({
<> <>
<button <button
type="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" position === "reverse" && "flex-row-reverse"
} ${className}`} } ${className}`}
disabled={disabled} disabled={disabled}

View File

@ -57,7 +57,7 @@ const TextArea: React.FC<Props> = ({
"w-full outline-none px-3 py-2 bg-transparent", "w-full outline-none px-3 py-2 bg-transparent",
mode === "primary" ? "border border-gray-300 rounded-md" : "", mode === "primary" ? "border border-gray-300 rounded-md" : "",
mode === "transparent" 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 ? "border-red-500" : "",
error && mode === "primary" ? "bg-red-100" : "", error && mode === "primary" ? "bg-red-100" : "",

View File

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