forked from github/plane
315 lines
15 KiB
TypeScript
315 lines
15 KiB
TypeScript
// 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.services";
|
|
// 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.services";
|
|
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;
|