mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge pull request #31 from aaryan610/master
style: cycles, list view, kanban card
This commit is contained in:
commit
1c30e53ef9
@ -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>
|
||||||
|
@ -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);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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}>
|
||||||
|
79
apps/app/components/project/cycles/BoardView/index.tsx
Normal file
79
apps/app/components/project/cycles/BoardView/index.tsx
Normal 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;
|
662
apps/app/components/project/cycles/BoardView/single-board.tsx
Normal file
662
apps/app/components/project/cycles/BoardView/single-board.tsx
Normal 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;
|
@ -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,7 +144,9 @@ 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) => {
|
||||||
|
// if (issue.cycle !== cycleId)
|
||||||
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
as="label"
|
as="label"
|
||||||
@ -155,19 +163,20 @@ const CycleIssuesListModal: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<input type="checkbox" checked={selected} readOnly />
|
<input type="checkbox" checked={selected} readOnly />
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 block rounded-full`}
|
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issue.state_detail.color,
|
backgroundColor: issue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||||
{activeProject?.identifier}-{issue.sequence_id}
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
{issue.name}
|
{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>
|
||||||
|
@ -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;
|
|
714
apps/app/components/project/cycles/ListView/index.tsx
Normal file
714
apps/app/components/project/cycles/ListView/index.tsx
Normal 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;
|
@ -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;
|
|
@ -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>
|
||||||
|
609
apps/app/components/project/issues/BoardView/single-board.tsx
Normal file
609
apps/app/components/project/issues/BoardView/single-board.tsx
Normal 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;
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,137 +52,141 @@ 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">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
colSpan={14}
|
|
||||||
scope="col"
|
|
||||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{selectedGroup === "state_detail.name" ? (
|
|
||||||
<span
|
|
||||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: states?.find((s) => s.name === singleGroup)?.color,
|
|
||||||
}}
|
}}
|
||||||
></span>
|
projectId={activeProject?.id as string}
|
||||||
) : null}
|
/>
|
||||||
|
<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"
|
{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>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
) : (
|
) : (
|
||||||
<thead className="bg-gray-100">
|
<h2 className="font-medium leading-5">All Issues</h2>
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
colSpan={14}
|
|
||||||
scope="col"
|
|
||||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
|
||||||
>
|
|
||||||
ALL ISSUES
|
|
||||||
<span className="ml-2 text-gray-500 font-normal text-sm">
|
|
||||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
)}
|
)}
|
||||||
<tbody className="bg-white">
|
<p className="text-gray-500 text-sm">
|
||||||
{groupedByIssues[singleGroup].length > 0
|
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||||
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => {
|
</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 = [
|
const assignees = [
|
||||||
...(issue?.assignees_list ?? []),
|
...(issue?.assignees_list ?? []),
|
||||||
...(issue?.assignees ?? []),
|
...(issue?.assignees ?? []),
|
||||||
]?.map(
|
]?.map((assignee) => {
|
||||||
(assignee) => people?.find((p) => p.member.id === assignee)?.member.email
|
const tempPerson = people?.find(
|
||||||
);
|
(p) => p.member.id === assignee
|
||||||
|
)?.member;
|
||||||
|
|
||||||
|
return {
|
||||||
|
avatar: tempPerson?.avatar,
|
||||||
|
first_name: tempPerson?.first_name,
|
||||||
|
email: tempPerson?.email,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className={classNames(
|
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
|
||||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
|
||||||
"border-t"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]">
|
<div className="flex items-center gap-2">
|
||||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
<span
|
||||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
|
||||||
</Link>
|
style={{
|
||||||
</td>
|
backgroundColor: issue.state_detail.color,
|
||||||
{Object.keys(properties).map(
|
}}
|
||||||
(key) =>
|
/>
|
||||||
properties[key as keyof Properties] && (
|
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
|
||||||
<React.Fragment key={key}>
|
<a className="group relative flex items-center gap-2">
|
||||||
{(key as keyof Properties) === "key" ? (
|
{properties.key && (
|
||||||
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||||
{activeProject?.identifier}-{issue.sequence_id}
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
</td>
|
</span>
|
||||||
) : (key as keyof Properties) === "priority" ? (
|
)}
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
<span className="">{issue.name}</span>
|
||||||
|
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1">Name</h5>
|
||||||
|
<div>{issue.name}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||||
|
{properties.priority && (
|
||||||
<Listbox
|
<Listbox
|
||||||
as="div"
|
as="div"
|
||||||
value={issue.priority}
|
value={issue.priority}
|
||||||
onChange={(data: string) => {
|
onChange={(data: string) => {
|
||||||
partialUpdateIssue({ priority: data }, issue.id);
|
partialUpdateIssue({ priority: data }, issue.id);
|
||||||
}}
|
}}
|
||||||
className="flex-shrink-0"
|
className="group relative flex-shrink-0"
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<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">
|
<Listbox.Button
|
||||||
<span
|
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 ${
|
||||||
className={classNames(
|
issue.priority === "urgent"
|
||||||
issue.priority ? "" : "text-gray-900",
|
? "bg-red-100 text-red-600"
|
||||||
"hidden truncate capitalize sm:block w-16"
|
: 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"}
|
{issue.priority ?? "None"}
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -195,136 +214,50 @@ const ListView: React.FC<Props> = ({
|
|||||||
</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 text-gray-900">
|
||||||
</Listbox>
|
Priority
|
||||||
</td>
|
</h5>
|
||||||
) : (key as keyof Properties) === "assignee" ? (
|
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
|
||||||
<Listbox
|
|
||||||
as="div"
|
|
||||||
value={issue.assignees}
|
|
||||||
onChange={(data: any) => {
|
|
||||||
const newData = issue.assignees ?? [];
|
|
||||||
if (newData.includes(data)) {
|
|
||||||
newData.splice(newData.indexOf(data), 1);
|
|
||||||
} else {
|
|
||||||
newData.push(data);
|
|
||||||
}
|
|
||||||
partialUpdateIssue(
|
|
||||||
{ assignees_list: newData },
|
|
||||||
issue.id
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border">
|
|
||||||
{() => {
|
|
||||||
if (assignees.length > 0)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{assignees.map((assignee, index) => (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
className={`capitalize ${
|
||||||
className={
|
issue.priority === "urgent"
|
||||||
"hidden truncate sm:block text-left"
|
? "text-red-600"
|
||||||
}
|
: issue.priority === "high"
|
||||||
>
|
? "text-orange-500"
|
||||||
{assignee}
|
: issue.priority === "medium"
|
||||||
</div>
|
? "text-yellow-500"
|
||||||
))}
|
: issue.priority === "low"
|
||||||
</>
|
? "text-green-500"
|
||||||
);
|
: ""
|
||||||
else return <span>None</span>;
|
|
||||||
}}
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={React.Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
|
||||||
{people?.map((person) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={person.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
classNames(
|
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
|
||||||
"cursor-pointer select-none px-3 py-2"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={person.member.id}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-x-1 ${
|
|
||||||
assignees.includes(
|
|
||||||
person.member.first_name
|
|
||||||
)
|
|
||||||
? "font-medium"
|
|
||||||
: "font-normal"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{person.member.avatar &&
|
{issue.priority ?? "None"}
|
||||||
person.member.avatar !== "" ? (
|
|
||||||
<div className="relative w-4 h-4">
|
|
||||||
<Image
|
|
||||||
src={person.member.avatar}
|
|
||||||
alt="avatar"
|
|
||||||
className="rounded-full"
|
|
||||||
layout="fill"
|
|
||||||
objectFit="cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
{person.member.first_name.charAt(0)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p>{person.member.first_name}</p>
|
|
||||||
</div>
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</td>
|
)}
|
||||||
) : (key as keyof Properties) === "state" ? (
|
{properties.state && (
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
|
||||||
<Listbox
|
<Listbox
|
||||||
as="div"
|
as="div"
|
||||||
value={issue.state}
|
value={issue.state}
|
||||||
onChange={(data: string) => {
|
onChange={(data: string) => {
|
||||||
partialUpdateIssue({ state: data }, issue.id);
|
partialUpdateIssue({ state: data }, issue.id);
|
||||||
}}
|
}}
|
||||||
className="flex-shrink-0"
|
className="group relative flex-shrink-0"
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Listbox.Button
|
<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">
|
||||||
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
|
<span
|
||||||
className={classNames(
|
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||||
issue.state ? "" : "text-gray-900",
|
style={{
|
||||||
"hidden capitalize sm:block w-16"
|
backgroundColor: issue.state_detail.color,
|
||||||
)}
|
}}
|
||||||
>
|
></span>
|
||||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -352,31 +285,213 @@ const ListView: React.FC<Props> = ({
|
|||||||
</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>
|
||||||
|
<div>{issue.state_detail.name}</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</td>
|
)}
|
||||||
) : (key as keyof Properties) === "target_date" ? (
|
{properties.start_date && (
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
<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
|
{issue.target_date
|
||||||
? renderShortNumericDateFormat(issue.target_date)
|
? renderShortNumericDateFormat(issue.target_date)
|
||||||
: "-"}
|
: "N/A"}
|
||||||
</td>
|
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1 text-gray-900">
|
||||||
|
Target date
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
{renderShortNumericDateFormat(issue.target_date ?? "")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{issue.target_date &&
|
||||||
|
(issue.target_date < new Date().toISOString()
|
||||||
|
? `Target date has passed by ${findHowManyDaysLeft(
|
||||||
|
issue.target_date
|
||||||
|
)} days`
|
||||||
|
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||||
|
? `Target date is in ${findHowManyDaysLeft(
|
||||||
|
issue.target_date
|
||||||
|
)} days`
|
||||||
|
: "Target date")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.assignee && (
|
||||||
|
<Listbox
|
||||||
|
as="div"
|
||||||
|
value={issue.assignees}
|
||||||
|
onChange={(data: any) => {
|
||||||
|
const newData = issue.assignees ?? [];
|
||||||
|
if (newData.includes(data)) {
|
||||||
|
newData.splice(newData.indexOf(data), 1);
|
||||||
|
} else {
|
||||||
|
newData.push(data);
|
||||||
|
}
|
||||||
|
partialUpdateIssue({ assignees_list: newData }, issue.id);
|
||||||
|
}}
|
||||||
|
className="group relative flex-shrink-0"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Listbox.Button>
|
||||||
|
<div className="flex items-center gap-1 text-xs cursor-pointer">
|
||||||
|
{assignees.length > 0 ? (
|
||||||
|
assignees.map((assignee, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||||
|
index !== 0 ? "-ml-2.5" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{assignee.avatar && assignee.avatar !== "" ? (
|
||||||
|
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||||
|
<Image
|
||||||
|
src={assignee.avatar}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
className="rounded-full"
|
||||||
|
alt={assignee?.first_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize">
|
<div
|
||||||
{issue[key as keyof IIssue] ??
|
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||||
(issue[key as keyof IIssue] as any)?.name ??
|
>
|
||||||
"None"}
|
{assignee.first_name?.charAt(0)}
|
||||||
</td>
|
</div>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||||
|
<Image
|
||||||
|
src={User}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
className="rounded-full"
|
||||||
|
alt="No user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute right-0 z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
|
{people?.map((person) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={person.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
|
"cursor-pointer select-none p-2"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
value={person.member.id}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-x-1 ${
|
||||||
|
assignees.includes({
|
||||||
|
avatar: person.member.avatar,
|
||||||
|
first_name: person.member.first_name,
|
||||||
|
email: person.member.email,
|
||||||
|
})
|
||||||
|
? "font-medium"
|
||||||
|
: "font-normal"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{person.member.avatar &&
|
||||||
|
person.member.avatar !== "" ? (
|
||||||
|
<div className="relative h-4 w-4">
|
||||||
|
<Image
|
||||||
|
src={person.member.avatar}
|
||||||
|
alt="avatar"
|
||||||
|
className="rounded-full"
|
||||||
|
layout="fill"
|
||||||
|
objectFit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
|
||||||
|
{person.member.first_name &&
|
||||||
|
person.member.first_name !== ""
|
||||||
|
? person.member.first_name.charAt(0)
|
||||||
|
: person.member.email.charAt(0)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<td className="px-3">
|
<p>
|
||||||
<div className="flex justify-end items-center gap-2">
|
{person.member.first_name &&
|
||||||
|
person.member.first_name !== ""
|
||||||
|
? person.member.first_name
|
||||||
|
: person.member.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1">Assigned to</h5>
|
||||||
|
<div>
|
||||||
|
{issue.assignee_details?.length > 0
|
||||||
|
? issue.assignee_details
|
||||||
|
.map((assignee) => assignee.first_name)
|
||||||
|
.join(", ")
|
||||||
|
: "No one"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<Menu.Button
|
||||||
|
as="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center bg-blue-100 text-blue-600 hover:bg-blue-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedIssue({
|
setSelectedIssue({
|
||||||
...issue,
|
...issue,
|
||||||
@ -384,30 +499,68 @@ const ListView: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-3 w-3" />
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center bg-red-100 text-red-600 hover:bg-red-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteIssue(issue.id);
|
handleDeleteIssue(issue.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrashIcon className="h-3 w-3" />
|
Delete permanently
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</Menu.Item>
|
||||||
</tr>
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: null}
|
) : (
|
||||||
</tbody>
|
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
|
||||||
</table>
|
)
|
||||||
</div>
|
) : (
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
<div className="mx-auto h-16 w-16 grid place-items-center rounded-full bg-red-100">
|
||||||
<ExclamationTriangleIcon
|
<ExclamationTriangleIcon
|
||||||
className="h-6 w-6 text-red-600"
|
className="h-8 w-8 text-red-600"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<Dialog.Title
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
as="h3"
|
||||||
Delete Issue
|
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>
|
@ -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,12 +230,13 @@ 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="h-full w-full divide-y-2 divide-gray-100">
|
||||||
<div className="flex justify-between items-center pb-3">
|
<div className="flex justify-between items-center pb-3">
|
||||||
<h4 className="text-sm font-medium">
|
<h4 className="text-sm font-medium">
|
||||||
@ -277,6 +287,13 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
|
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
</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">
|
||||||
@ -404,10 +421,16 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||||||
: "text-gray-900"
|
: "text-gray-900"
|
||||||
} ${
|
} ${
|
||||||
item.label === "Priority" && "capitalize"
|
item.label === "Priority" && "capitalize"
|
||||||
} cursor-pointer select-none relative p-2 rounded-md truncate`
|
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||||
}
|
}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
>
|
>
|
||||||
|
{option.color && (
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||||
|
style={{ backgroundColor: option.color }}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
{option.label}
|
{option.label}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))
|
))
|
||||||
@ -432,70 +455,31 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 pt-3">
|
<div className="pt-3 space-y-3">
|
||||||
<h5 className="text-xs font-medium">Add new label</h5>
|
<div className="flex justify-between items-start">
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Title"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "This is required",
|
|
||||||
}}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
+
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-x-2 text-sm basis-1/2">
|
<div className="flex items-center gap-x-2 text-sm basis-1/2">
|
||||||
<TagIcon className="w-4 h-4" />
|
<TagIcon className="w-4 h-4" />
|
||||||
<p>Label</p>
|
<p>Label</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-1/2">
|
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="labels_list"
|
name="labels_list"
|
||||||
@ -511,23 +495,15 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
||||||
<div className="relative">
|
<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">
|
<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
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
value ? "" : "text-gray-900",
|
value ? "" : "text-gray-900",
|
||||||
"hidden truncate capitalize sm:block text-left"
|
"hidden sm:block text-left"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{value && value.length > 0
|
Select Label
|
||||||
? value
|
|
||||||
.map(
|
|
||||||
(i: string) =>
|
|
||||||
issueLabels?.find((option) => option.id === i)?.name
|
|
||||||
)
|
|
||||||
.join(", ")
|
|
||||||
: "None"}
|
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon className="h-3 w-3" />
|
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -578,7 +554,87 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
|
||||||
|
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
{createLabelForm ? (
|
||||||
|
<>
|
||||||
|
<XMarkIcon className="h-3 w-3" /> Cancel
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlusIcon className="h-3 w-3" /> Create new
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{createLabelForm && (
|
||||||
|
<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-5 h-5 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: watch("colour") ?? "green",
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" />
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Title"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "This is required",
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Button type="submit" theme="success" disabled={isSubmitting}>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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)}
|
||||||
|
@ -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" />
|
||||||
|
@ -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);
|
||||||
|
193
apps/app/components/sidebar/projects-list.tsx
Normal file
193
apps/app/components/sidebar/projects-list.tsx
Normal 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;
|
293
apps/app/components/sidebar/workspace-options.tsx
Normal file
293
apps/app/components/sidebar/workspace-options.tsx
Normal 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;
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
21
apps/app/layouts/Navbar/Header.tsx
Normal file
21
apps/app/layouts/Navbar/Header.tsx
Normal 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;
|
201
apps/app/layouts/Navbar/main-sidebar.tsx
Normal file
201
apps/app/layouts/Navbar/main-sidebar.tsx
Normal 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;
|
41
apps/app/layouts/Navbar/settings-sidebar.tsx
Normal file
41
apps/app/layouts/Navbar/settings-sidebar.tsx
Normal 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;
|
@ -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} />
|
||||||
|
<div
|
||||||
|
className={`w-full flex-grow ${noPadding ? "" : "p-5"} ${
|
||||||
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
70
apps/app/layouts/settings-layout.tsx
Normal file
70
apps/app/layouts/settings-layout.tsx
Normal 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;
|
2
apps/app/layouts/types.d.ts
vendored
2
apps/app/layouts/types.d.ts
vendored
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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) ?? [],
|
||||||
]) ?? []
|
]) ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addIssueToSprint(
|
async addIssueToCycle(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
cycleId: string,
|
cycleId: string,
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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
|
||||||
|
@ -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,80 +71,14 @@ const MyIssues: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWorkspaceChange = (workspaceId: string | null) => {
|
|
||||||
setSelectedWorkspace(workspaceId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout
|
||||||
<div className="w-full h-full flex flex-col space-y-5">
|
breadcrumbs={
|
||||||
{myIssues ? (
|
|
||||||
<>
|
|
||||||
{myIssues.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="My Issues" />
|
<BreadcrumbItem title="My Issues" />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
}
|
||||||
<h2 className="text-2xl font-medium">My Issues</h2>
|
right={
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<Menu as="div" className="relative inline-block w-40">
|
|
||||||
<div className="w-full">
|
|
||||||
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none">
|
|
||||||
<span className="flex gap-x-1 items-center">
|
|
||||||
{workspaces?.find((w) => w.id === selectedWorkspace)?.name ??
|
|
||||||
"All workspaces"}
|
|
||||||
</span>
|
|
||||||
<div className="flex-grow flex justify-end">
|
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
|
||||||
<div className="p-1">
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${
|
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
|
||||||
} group flex w-full items-center rounded-md p-2 text-xs`}
|
|
||||||
onClick={() => handleWorkspaceChange(null)}
|
|
||||||
>
|
|
||||||
All workspaces
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
{workspaces &&
|
|
||||||
workspaces.map((workspace) => (
|
|
||||||
<Menu.Item key={workspace.id}>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${
|
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
|
||||||
} group flex w-full items-center rounded-md p-2 text-xs`}
|
|
||||||
onClick={() => handleWorkspaceChange(workspace.id)}
|
|
||||||
>
|
|
||||||
{workspace.name}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
Icon={PlusIcon}
|
Icon={PlusIcon}
|
||||||
label="Add Issue"
|
label="Add Issue"
|
||||||
@ -156,9 +90,13 @@ const MyIssues: NextPage = () => {
|
|||||||
document.dispatchEvent(e);
|
document.dispatchEvent(e);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
</div>
|
>
|
||||||
<div className="mt-4 flex flex-col">
|
<div className="w-full h-full flex flex-col space-y-5">
|
||||||
|
{myIssues ? (
|
||||||
|
<>
|
||||||
|
{myIssues.length > 0 ? (
|
||||||
|
<div className="flex flex-col">
|
||||||
<div className="overflow-x-auto ">
|
<div className="overflow-x-auto ">
|
||||||
<div className="inline-block min-w-full align-middle px-0.5 py-2">
|
<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">
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||||
@ -198,11 +136,7 @@ const MyIssues: NextPage = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white">
|
<tbody className="bg-white">
|
||||||
{myIssues
|
{myIssues.map((myIssue, index) => (
|
||||||
.filter((i) =>
|
|
||||||
selectedWorkspace ? i.workspace === selectedWorkspace : true
|
|
||||||
)
|
|
||||||
.map((myIssue, index) => (
|
|
||||||
<tr
|
<tr
|
||||||
key={myIssue.id}
|
key={myIssue.id}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -211,9 +145,7 @@ const MyIssues: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
||||||
<Link
|
<Link href={`/projects/${myIssue.project}/issues/${myIssue.id}`}>
|
||||||
href={`/projects/${myIssue.project}/issues/${myIssue.id}`}
|
|
||||||
>
|
|
||||||
<a>{myIssue.name}</a>
|
<a>{myIssue.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@ -240,7 +172,6 @@ const MyIssues: NextPage = () => {
|
|||||||
</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
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
416
apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx
Normal file
416
apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx
Normal 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;
|
@ -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">
|
|
||||||
<Breadcrumbs>
|
|
||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
|
||||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
|
|
||||||
</Breadcrumbs>
|
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
|
||||||
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<DragDropContext onDragEnd={handleDragEnd}>
|
|
||||||
{cycles.map((cycle) => (
|
{cycles.map((cycle) => (
|
||||||
<CycleView
|
<Link key={cycle.id} href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
|
||||||
key={cycle.id}
|
<a className="block bg-white p-3 rounded-md">{cycle.name}</a>
|
||||||
cycle={cycle}
|
</Link>
|
||||||
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">
|
@ -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,25 +213,13 @@ const IssueDetail: NextPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// console.log(issueDetail);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout noPadding={true} bg="secondary">
|
<AppLayout
|
||||||
<CreateUpdateIssuesModal
|
noPadding={true}
|
||||||
isOpen={isOpen}
|
bg="secondary"
|
||||||
setIsOpen={setIsOpen}
|
breadcrumbs={
|
||||||
projectId={projectId as string}
|
|
||||||
prePopulateData={{
|
|
||||||
...preloadedData,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AddAsSubIssue
|
|
||||||
isOpen={isAddAsSubIssueOpen}
|
|
||||||
setIsOpen={setIsAddAsSubIssueOpen}
|
|
||||||
parent={issueDetail}
|
|
||||||
/>
|
|
||||||
{issueDetail && activeProject ? (
|
|
||||||
<div className="flex gap-5">
|
|
||||||
<div className="basis-3/4 space-y-5 p-5">
|
|
||||||
<div className="mb-5">
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
title={`${activeProject?.name ?? "Project"} Issues`}
|
title={`${activeProject?.name ?? "Project"} Issues`}
|
||||||
@ -238,7 +231,54 @@ const IssueDetail: NextPage = () => {
|
|||||||
} Details`}
|
} Details`}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</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>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CreateUpdateIssuesModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
projectId={projectId as string}
|
||||||
|
prePopulateData={{
|
||||||
|
...preloadedData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ConfirmIssueDeletion
|
||||||
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
|
isOpen={deleteIssueModal}
|
||||||
|
data={issueDetail}
|
||||||
|
/>
|
||||||
|
<AddAsSubIssue
|
||||||
|
isOpen={isAddAsSubIssueOpen}
|
||||||
|
setIsOpen={setIsAddAsSubIssueOpen}
|
||||||
|
parent={issueDetail}
|
||||||
|
/>
|
||||||
|
{issueDetail && activeProject ? (
|
||||||
|
<div className="flex gap-5">
|
||||||
|
<div className="basis-3/4 space-y-5 p-5">
|
||||||
|
<div className="mb-5"></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>
|
||||||
|
@ -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,32 +153,15 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout
|
||||||
<CreateUpdateIssuesModal
|
breadcrumbs={
|
||||||
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
projectId={projectId as string}
|
|
||||||
data={selectedIssue}
|
|
||||||
/>
|
|
||||||
<ConfirmIssueDeletion
|
|
||||||
handleClose={() => setDeleteIssue(undefined)}
|
|
||||||
isOpen={!!deleteIssue}
|
|
||||||
data={projectIssues?.results.find((issue) => issue.id === deleteIssue)}
|
|
||||||
/>
|
|
||||||
{!projectIssues ? (
|
|
||||||
<div className="h-full w-full flex justify-center items-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
) : projectIssues.count > 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="w-full space-y-5 mb-5">
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
<BreadcrumbItem title="Projects" link="/projects" />
|
||||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Issues`} />
|
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Issues`} />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className="flex items-center justify-between w-full">
|
}
|
||||||
<h2 className="text-2xl font-medium">Project Issues</h2>
|
right={
|
||||||
<div className="flex items-center md:gap-x-6 sm:gap-x-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -180,18 +193,12 @@ const ProjectIssues: NextPage = () => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
open ? "text-gray-900" : "text-gray-500",
|
open ? "bg-gray-100 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"
|
"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>
|
<span>View</span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
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>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -203,14 +210,14 @@ const ProjectIssues: NextPage = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
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">
|
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<h4 className="text-base text-gray-600">Group by</h4>
|
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
groupByOptions.find((option) => option.key === groupByProperty)
|
groupByOptions.find((option) => option.key === groupByProperty)?.name ??
|
||||||
?.name ?? "Select"
|
"Select"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{groupByOptions.map((option) => (
|
{groupByOptions.map((option) => (
|
||||||
@ -223,8 +230,8 @@ const ProjectIssues: NextPage = () => {
|
|||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<h4 className="text-base text-gray-600">Order by</h4>
|
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||||
@ -232,8 +239,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{orderByOptions.map((option) =>
|
{orderByOptions.map((option) =>
|
||||||
groupByProperty === "priority" &&
|
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||||
option.key === "priority" ? null : (
|
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setOrderBy(option.key)}
|
onClick={() => setOrderBy(option.key)}
|
||||||
@ -244,12 +250,12 @@ const ProjectIssues: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<h4 className="text-base text-gray-600">Issue type</h4>
|
<h4 className="text-sm text-gray-600">Issue type</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
filterIssueOptions.find((option) => option.key === filterIssue)
|
filterIssueOptions.find((option) => option.key === filterIssue)?.name ??
|
||||||
?.name ?? "Select"
|
"Select"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{filterIssueOptions.map((option) => (
|
{filterIssueOptions.map((option) => (
|
||||||
@ -265,14 +271,14 @@ const ProjectIssues: NextPage = () => {
|
|||||||
<div className="border-b-2"></div>
|
<div className="border-b-2"></div>
|
||||||
<div className="relative flex flex-col gap-1">
|
<div className="relative flex flex-col gap-1">
|
||||||
<h4 className="text-base text-gray-600">Properties</h4>
|
<h4 className="text-base text-gray-600">Properties</h4>
|
||||||
<div>
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{Object.keys(properties).map((key) => (
|
{Object.keys(properties).map((key) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
|
className={`px-2 py-1 capitalize rounded border border-theme text-xs ${
|
||||||
properties[key as keyof Properties]
|
properties[key as keyof Properties]
|
||||||
? "border-indigo-600 bg-indigo-600 text-white"
|
? "border-theme bg-theme text-white"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
@ -300,8 +306,25 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CreateUpdateIssuesModal
|
||||||
|
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
projectId={projectId as string}
|
||||||
|
data={selectedIssue}
|
||||||
|
/>
|
||||||
|
<ConfirmIssueDeletion
|
||||||
|
handleClose={() => setDeleteIssue(undefined)}
|
||||||
|
isOpen={!!deleteIssue}
|
||||||
|
data={projectIssues?.results.find((issue) => issue.id === deleteIssue)}
|
||||||
|
/>
|
||||||
|
{!projectIssues ? (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : projectIssues.count > 0 ? (
|
||||||
|
<>
|
||||||
{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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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,15 +26,6 @@ import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
|
|||||||
// types
|
// types
|
||||||
import type { IProject, IWorkspace } from "types";
|
import type { IProject, IWorkspace } from "types";
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
identifier: "",
|
|
||||||
network: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectSettings: NextPage = () => {
|
|
||||||
// FIXME: instead of using dynamic import inside component use it outside
|
|
||||||
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
|
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
|
||||||
loading: () => <p>Loading...</p>,
|
loading: () => <p>Loading...</p>,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -55,6 +46,14 @@ const ProjectSettings: NextPage = () => {
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultValues: Partial<IProject> = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
identifier: "",
|
||||||
|
network: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectSettings: NextPage = () => {
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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
BIN
apps/app/public/user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
@ -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",
|
||||||
},
|
},
|
||||||
|
3
apps/app/types/issues.d.ts
vendored
3
apps/app/types/issues.d.ts
vendored
@ -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;
|
||||||
|
13
apps/app/types/sprints.d.ts
vendored
13
apps/app/types/sprints.d.ts
vendored
@ -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;
|
||||||
|
5
apps/app/types/workspace.d.ts
vendored
5
apps/app/types/workspace.d.ts
vendored
@ -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[];
|
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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" />
|
||||||
|
@ -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"> →</span>
|
<span aria-hidden="true"> →</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -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}
|
||||||
|
@ -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" : "",
|
||||||
|
2
apps/app/ui/TextArea/types.d.ts
vendored
2
apps/app/ui/TextArea/types.d.ts
vendored
@ -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>>;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user