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:
Anmol Singh Bhatia 2023-03-30 18:59:53 +05:30 committed by GitHub
parent f9ee898d88
commit 02e6439bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 5 deletions

View File

@ -18,6 +18,7 @@ import { AllLists, AllBoards, FilterList } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
import { TransferIssuesModal } from "components/cycles";
// ui // ui
import { EmptySpace, EmptySpaceItem, PrimaryButton, Spinner } from "components/ui"; import { EmptySpace, EmptySpaceItem, PrimaryButton, Spinner } from "components/ui";
import { CalendarView } from "./calendar-view"; import { CalendarView } from "./calendar-view";
@ -28,7 +29,7 @@ import {
RectangleStackIcon, RectangleStackIcon,
TrashIcon, TrashIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ExclamationIcon } from "components/icons"; import { ExclamationIcon, getStateGroupIcon, TransferIcon } from "components/icons";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// types // types
@ -82,6 +83,9 @@ export const IssuesView: React.FC<Props> = ({
// trash box // trash box
const [trashBox, setTrashBox] = useState(false); const [trashBox, setTrashBox] = useState(false);
// transfer issue
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -406,6 +410,10 @@ export const IssuesView: React.FC<Props> = ({
isOpen={deleteIssueModal} isOpen={deleteIssueModal}
data={issueToDelete} data={issueToDelete}
/> />
<TransferIssuesModal
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
<div className="mb-5 -mt-4"> <div className="mb-5 -mt-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<FilterList filters={filters} setFilters={setFilters} /> <FilterList filters={filters} setFilters={setFilters} />
@ -460,9 +468,17 @@ export const IssuesView: React.FC<Props> = ({
isNotEmpty ? ( isNotEmpty ? (
<> <>
{isCompleted && ( {isCompleted && (
<div className="mb-4 flex items-center gap-2 text-sm text-gray-500"> <div className="flex items-center justify-between mb-4">
<ExclamationIcon height={14} width={14} /> <div className="flex items-center gap-2 text-sm text-gray-500">
<span>Completed cycles are not editable.</span> <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> </div>
)} )}
{issueView === "list" ? ( {issueView === "list" ? (

View File

@ -7,4 +7,5 @@ export * from "./select";
export * from "./sidebar"; export * from "./sidebar";
export * from "./single-cycle-card"; export * from "./single-cycle-card";
export * from "./empty-cycle"; export * from "./empty-cycle";
export * from "./date"; export * from "./transfer-issues-modal";
export * from "./date";

View 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>
);
};

View File

@ -56,3 +56,4 @@ export * from "./users";
export * from "./import-layers"; export * from "./import-layers";
export * from "./check"; export * from "./check";
export * from "./water-drop-icon"; export * from "./water-drop-icon";
export * from "./transfer-icon";

View 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>
);

View File

@ -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( async removeCycleFromFavorites(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,