forked from github/plane
feat: completed cycle transfer issue (#624)
* feat: bulk transfer issue for completed cycle added * feat: toast alert added for issue transfer
This commit is contained in:
parent
f9ee898d88
commit
02e6439bd5
@ -18,6 +18,7 @@ import { AllLists, AllBoards, FilterList } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
import { TransferIssuesModal } from "components/cycles";
|
||||
// ui
|
||||
import { EmptySpace, EmptySpaceItem, PrimaryButton, Spinner } from "components/ui";
|
||||
import { CalendarView } from "./calendar-view";
|
||||
@ -28,7 +29,7 @@ import {
|
||||
RectangleStackIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
import { ExclamationIcon, getStateGroupIcon, TransferIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
@ -82,6 +83,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
// trash box
|
||||
const [trashBox, setTrashBox] = useState(false);
|
||||
|
||||
// transfer issue
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
@ -406,6 +410,10 @@ export const IssuesView: React.FC<Props> = ({
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueToDelete}
|
||||
/>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<div className="mb-5 -mt-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
@ -460,9 +468,17 @@ export const IssuesView: React.FC<Props> = ({
|
||||
isNotEmpty ? (
|
||||
<>
|
||||
{isCompleted && (
|
||||
<div className="mb-4 flex items-center gap-2 text-sm text-gray-500">
|
||||
<ExclamationIcon height={14} width={14} />
|
||||
<span>Completed cycles are not editable.</span>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<ExclamationIcon height={14} width={14} />
|
||||
<span>Completed cycles are not editable.</span>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton onClick={()=>setTransferIssuesModal(true)} className="flex items-center gap-3 rounded-lg">
|
||||
<TransferIcon className="h-4 w-4" />
|
||||
<span>Transfer Issues</span>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{issueView === "list" ? (
|
||||
|
@ -7,4 +7,5 @@ export * from "./select";
|
||||
export * from "./sidebar";
|
||||
export * from "./single-cycle-card";
|
||||
export * from "./empty-cycle";
|
||||
export * from "./date";
|
||||
export * from "./transfer-issues-modal";
|
||||
export * from "./date";
|
153
apps/app/components/cycles/transfer-issues-modal.tsx
Normal file
153
apps/app/components/cycles/transfer-issues-modal.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// component
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
//icons
|
||||
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { ContrastIcon, CyclesIcon } from "components/icons";
|
||||
// fetch-key
|
||||
import { CYCLE_INCOMPLETE_LIST } from "constants/fetch-keys";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
|
||||
const transferIssue = async (payload: any) => {
|
||||
await cyclesService
|
||||
.transferIssues(workspaceSlug as string, projectId as string, cycleId as string, payload)
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Issues transfered successfully",
|
||||
message:
|
||||
"Issues have been transferred successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"Issues cannot be transfer. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const { data: incompleteCycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? incompleteCycles
|
||||
: incompleteCycles?.filter((option) =>
|
||||
option.name.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
}, [handleClose]);
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10">
|
||||
<div className="mt-10 flex min-h-full items-start justify-center p-4 text-center sm:p-0 md:mt-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-white p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-gray-700 text-base">Transfer Issues</h4>
|
||||
<button onClick={handleClose}>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-gray-500" />
|
||||
<input
|
||||
className="outline-none"
|
||||
placeholder="Search for a cycle..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start w-full gap-2">
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option: ICycle) => (
|
||||
<button
|
||||
key={option.id}
|
||||
className="flex items-center gap-4 px-4 py-3 text-gray-600 text-sm rounded w-full hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
transferIssue({
|
||||
new_cycle_id: option.id,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ContrastIcon className="h-5 w-5" />
|
||||
<span>{option.name}</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center text-gray-500">No matching results</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-gray-500">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
@ -56,3 +56,4 @@ export * from "./users";
|
||||
export * from "./import-layers";
|
||||
export * from "./check";
|
||||
export * from "./water-drop-icon";
|
||||
export * from "./transfer-icon";
|
16
apps/app/components/icons/transfer-icon.tsx
Normal file
16
apps/app/components/icons/transfer-icon.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const TransferIcon: React.FC<Props> = ({ width, height, className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 18 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6.16683 14.6667C4.54183 14.6667 3.16336 14.1007 2.03141 12.9688C0.899468 11.8368 0.333496 10.4583 0.333496 8.83333C0.333496 7.125 0.941135 5.73264 2.15641 4.65625C3.37169 3.57986 4.72933 3.09028 6.22933 3.1875L4.87516 1.83333C4.75016 1.70833 4.68766 1.55903 4.68766 1.38542C4.68766 1.21181 4.75016 1.0625 4.87516 0.9375C5.00016 0.8125 5.14947 0.75 5.32308 0.75C5.49669 0.75 5.646 0.8125 5.771 0.9375L8.22933 3.39583C8.29877 3.46528 8.34739 3.53472 8.37516 3.60417C8.40294 3.67361 8.41683 3.75 8.41683 3.83333C8.41683 3.91667 8.40294 3.99306 8.37516 4.0625C8.34739 4.13194 8.29877 4.20139 8.22933 4.27083L5.771 6.72917C5.646 6.85417 5.50016 6.91319 5.3335 6.90625C5.16683 6.89931 5.021 6.83333 4.896 6.70833C4.771 6.58333 4.7085 6.43403 4.7085 6.26042C4.7085 6.08681 4.771 5.9375 4.896 5.8125L6.29183 4.41667C4.97239 4.38889 3.8578 4.79167 2.94808 5.625C2.03836 6.45833 1.5835 7.52778 1.5835 8.83333C1.5835 10.0972 2.03141 11.1771 2.92725 12.0729C3.82308 12.9688 4.90294 13.4167 6.16683 13.4167H8.04183C8.22239 13.4167 8.37169 13.4757 8.48975 13.5938C8.6078 13.7118 8.66683 13.8611 8.66683 14.0417C8.66683 14.2222 8.6078 14.3715 8.48975 14.4896C8.37169 14.6076 8.22239 14.6667 8.04183 14.6667H6.16683ZM11.5835 14.6667C11.2363 14.6667 10.9411 14.5451 10.6981 14.3021C10.455 14.059 10.3335 13.7639 10.3335 13.4167V10.0833C10.3335 9.73611 10.455 9.44097 10.6981 9.19792C10.9411 8.95486 11.2363 8.83333 11.5835 8.83333H16.5835C16.9307 8.83333 17.2259 8.95486 17.4689 9.19792C17.712 9.44097 17.8335 9.73611 17.8335 10.0833V13.4167C17.8335 13.7639 17.712 14.059 17.4689 14.3021C17.2259 14.5451 16.9307 14.6667 16.5835 14.6667H11.5835ZM11.5835 13.4167H16.5835V10.0833H11.5835V13.4167ZM11.5835 7.16667C11.2363 7.16667 10.9411 7.04514 10.6981 6.80208C10.455 6.55903 10.3335 6.26389 10.3335 5.91667V2.58333C10.3335 2.23611 10.455 1.94097 10.6981 1.69792C10.9411 1.45486 11.2363 1.33333 11.5835 1.33333H16.5835C16.9307 1.33333 17.2259 1.45486 17.4689 1.69792C17.712 1.94097 17.8335 2.23611 17.8335 2.58333V5.91667C17.8335 6.26389 17.712 6.55903 17.4689 6.80208C17.2259 7.04514 16.9307 7.16667 16.5835 7.16667H11.5835Z" fill="white"/>
|
||||
</svg>
|
||||
);
|
@ -210,6 +210,24 @@ class ProjectCycleServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async transferIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
data: {
|
||||
new_cycle_id: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/transfer-issues/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeCycleFromFavorites(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
Loading…
Reference in New Issue
Block a user