fix: cycles views list and board

This commit is contained in:
sriram veeraghanta 2023-10-02 20:33:28 +05:30
parent a39aa80e76
commit 9c2ea8a7ae
10 changed files with 559 additions and 89 deletions

View File

@ -0,0 +1,366 @@
import React, { FC } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// components
import { SingleProgressStats } from "components/core";
// ui
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
import { AssigneesList } from "components/ui/avatar";
import { RadialProgressBar } from "@plane/ui";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import {
TargetIcon,
ContrastIcon,
PersonRunningIcon,
ArrowRightIcon,
TriangleExclamationIcon,
AlarmClockIcon,
} from "components/icons";
import { ChevronDownIcon, LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types
import { ICycle } from "types";
const stateGroups = [
{
key: "backlog_issues",
title: "Backlog",
color: "#dee2e6",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#26b5ce",
},
{
key: "started_issues",
title: "Started",
color: "#f7ae59",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#d687ff",
},
{
key: "completed_issues",
title: "Completed",
color: "#09a953",
},
];
export interface ICyclesBoardCard {
cycle: ICycle;
filter: string;
}
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const { cycle } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// toast
const { setToastAlert } = useToast();
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Cycle link copied to clipboard.",
});
});
};
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
color: group.color,
}));
const groupedIssues: any = {
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
started: cycle.started_issues,
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
};
const handleRemoveFromFavorites = () => {};
const handleAddToFavorites = () => {};
const handleEditCycle = () => {};
const handleDeleteCycle = () => {};
return (
<div>
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-200 text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-1">
<span className="h-5 w-5">
<ContrastIcon
className="h-5 w-5"
color={`${
cycleStatus === "current"
? "#09A953"
: cycleStatus === "upcoming"
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
</span>
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 15)}</h3>
</Tooltip>
</span>
<span className="flex items-center gap-1 capitalize">
<span
className={`rounded-full px-1.5 py-0.5
${
cycleStatus === "current"
? "bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
<PersonRunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1 whitespace-nowrap">
<AlarmClockIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
</span>
) : cycleStatus === "completed" ? (
<span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues - cycle.completed_issues > 0 && (
<Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
}`}
>
<span>
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
</span>
</Tooltip>
)}{" "}
Completed
</span>
) : (
cycleStatus
)}
</span>
{cycle.is_favorite ? (
<button onClick={handleRemoveFromFavorites}>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={handleAddToFavorites}>
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button>
)}
</span>
</div>
<div className="flex h-4 items-center justify-start gap-5 text-custom-text-200">
{cycleStatus !== "draft" && (
<>
<div className="flex items-start gap-1">
<CalendarDaysIcon className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<ArrowRightIcon className="h-4 w-4" />
<div className="flex items-start gap-1">
<TargetIcon className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</>
)}
</div>
<div className="flex justify-between items-end">
<div className="flex flex-col gap-2 text-xs text-custom-text-200">
<div className="flex items-center gap-2">
<div className="w-16">Creator:</div>
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
</div>
<div className="flex h-5 items-center gap-2">
<div className="w-16">Members:</div>
{cycle.assignees.length > 0 ? (
<div className="flex items-center gap-1 text-custom-text-200">
<AssigneesList users={cycle.assignees} length={4} />
</div>
) : (
"No members"
)}
</div>
</div>
<div className="flex items-center">
{!isCompleted && (
<button
onClick={handleEditCycle}
className="cursor-pointer rounded p-1 text-custom-text-200 duration-300 hover:bg-custom-background-80"
>
<PencilIcon className="h-4 w-4" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
{!isCompleted && (
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
handleCopyText();
}}
>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
</a>
</Link>
<div className="flex h-full flex-col rounded-b-[10px]">
<Disclosure>
{({ open }) => (
<div
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-200 bg-custom-background-80 text-custom-text-200 ${
open ? "" : "flex-row"
}`}
>
<div className="flex w-full items-center gap-2 px-4 py-1">
<span>Progress</span>
<Tooltip
tooltipContent={
<div className="flex w-56 flex-col">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: stateGroups[index].color,
}}
/>
<span className="text-xs capitalize">{group}</span>
</div>
}
completed={groupedIssues[group]}
total={cycle.total_issues}
/>
))}
</div>
}
position="bottom"
>
<div className="flex w-full items-center">
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
</div>
</Tooltip>
<Disclosure.Button>
<span className="p-1">
<ChevronDownIcon className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</span>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
<div className="overflow-hidden rounded-b-md bg-custom-background-80 py-3 shadow">
<div className="col-span-2 space-y-3 px-4">
<div className="space-y-3 text-xs">
{stateGroups.map((group) => (
<div key={group.key} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: group.color,
}}
/>
<h6 className="text-xs">{group.title}</h6>
</div>
<div>
<span>
{cycle[group.key as keyof ICycle] as number}{" "}
<span className="text-custom-text-200">
-{" "}
{cycle.total_issues > 0
? `${Math.round(
((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,53 @@
import { FC } from "react";
// types
import { ICycle } from "types";
// components
import { CyclesBoardCard } from "components/cycles";
export interface ICyclesBoard {
cycles: ICycle[];
filter: string;
}
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
const { cycles, filter } = props;
return (
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{cycles.length > 0 ? (
<>
{cycles.map((cycle) => (
<CyclesBoardCard key={cycle.id} cycle={cycle} filter={filter} />
))}
</>
) : (
<div className="h-full grid place-items-center text-center">
<div className="space-y-2">
<div className="mx-auto flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
<path
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
fill="rgb(var(--color-text-400))"
/>
</svg>
</div>
<h4 className="text-sm text-custom-text-200">{filter === "all" ? "No cycles" : `No ${filter} cycles`}</h4>
<button
type="button"
className="text-custom-primary-100 text-sm outline-none"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
>
Create a new cycle
</button>
</div>
</div>
)}
</div>
);
};

View File

@ -1,4 +1,4 @@
import { FC, useEffect, useState } from "react"; import { FC } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
@ -23,12 +23,12 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
type TCycledListItem = { type TCyclesListItem = {
cycle: ICycle; cycle: ICycle;
handleEditCycle: () => void; handleEditCycle?: () => void;
handleDeleteCycle: () => void; handleDeleteCycle?: () => void;
handleAddToFavorites: () => void; handleAddToFavorites?: () => void;
handleRemoveFromFavorites: () => void; handleRemoveFromFavorites?: () => void;
}; };
const stateGroups = [ const stateGroups = [
@ -59,12 +59,12 @@ const stateGroups = [
}, },
]; ];
export const CycledListItem: FC<TCycledListItem> = (props) => { export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle, handleEditCycle, handleDeleteCycle, handleAddToFavorites, handleRemoveFromFavorites } = props; const { cycle } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
@ -91,6 +91,11 @@ export const CycledListItem: FC<TCycledListItem> = (props) => {
color: group.color, color: group.color,
})); }));
const handleAddToFavorites = () => {};
const handleRemoveFromFavorites = () => {};
const handleEditCycle = () => {};
const handleDeleteCycle = () => {};
return ( return (
<div> <div>
<div className="flex flex-col text-xs hover:bg-custom-background-80"> <div className="flex flex-col text-xs hover:bg-custom-background-80">
@ -246,33 +251,18 @@ export const CycledListItem: FC<TCycledListItem> = (props) => {
</span> </span>
</Tooltip> </Tooltip>
{cycle.is_favorite ? ( {cycle.is_favorite ? (
<button <button type="button" onClick={handleRemoveFromFavorites}>
onClick={(e) => {
e.preventDefault();
handleRemoveFromFavorites();
}}
>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" /> <StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button> </button>
) : ( ) : (
<button <button type="button" onClick={handleAddToFavorites}>
onClick={(e) => {
e.preventDefault();
handleAddToFavorites();
}}
>
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" /> <StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button> </button>
)} )}
<div className="flex items-center"> <div className="flex items-center">
<CustomMenu width="auto" verticalEllipsis> <CustomMenu width="auto" verticalEllipsis>
{!isCompleted && ( {!isCompleted && (
<CustomMenu.MenuItem <CustomMenu.MenuItem onClick={handleEditCycle}>
onClick={(e) => {
e.preventDefault();
handleEditCycle();
}}
>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" /> <PencilIcon className="h-4 w-4" />
<span>Edit Cycle</span> <span>Edit Cycle</span>
@ -280,24 +270,14 @@ export const CycledListItem: FC<TCycledListItem> = (props) => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
{!isCompleted && ( {!isCompleted && (
<CustomMenu.MenuItem <CustomMenu.MenuItem onClick={handleDeleteCycle}>
onClick={(e) => {
e.preventDefault();
handleDeleteCycle();
}}
>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
<span>Delete cycle</span> <span>Delete cycle</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
<CustomMenu.MenuItem <CustomMenu.MenuItem onClick={handleCopyText}>
onClick={(e) => {
e.preventDefault();
handleCopyText();
}}
>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" /> <LinkIcon className="h-4 w-4" />
<span>Copy cycle link</span> <span>Copy cycle link</span>

View File

@ -3,13 +3,15 @@ import { FC } from "react";
import { Loader } from "components/ui"; import { Loader } from "components/ui";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
import { CyclesListItem } from "./cycles-list-item";
export interface ICyclesList { export interface ICyclesList {
cycles: ICycle[]; cycles: ICycle[];
filter: string;
} }
export const CyclesList: FC<ICyclesList> = (props) => { export const CyclesList: FC<ICyclesList> = (props) => {
const { cycles } = props; const { cycles, filter } = props;
return ( return (
<div> <div>
@ -18,16 +20,9 @@ export const CyclesList: FC<ICyclesList> = (props) => {
{cycles.length > 0 ? ( {cycles.length > 0 ? (
<div className="divide-y divide-custom-border-200"> <div className="divide-y divide-custom-border-200">
{cycles.map((cycle) => ( {cycles.map((cycle) => (
<div className="hover:bg-custom-background-80"> <div className="hover:bg-custom-background-80" key={cycle.id}>
<div className="flex flex-col border-custom-border-200"> <div className="flex flex-col border-custom-border-200">
<SingleCycleList <CyclesListItem cycle={cycle} />
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
handleAddToFavorites={() => handleAddToFavorites(cycle)}
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
/>
</div> </div>
</div> </div>
))} ))}
@ -45,7 +40,7 @@ export const CyclesList: FC<ICyclesList> = (props) => {
</svg> </svg>
</div> </div>
<h4 className="text-sm text-custom-text-200"> <h4 className="text-sm text-custom-text-200">
{cycleTab === "all" ? "No cycles" : `No ${cycleTab} cycles`} {filter === "all" ? "No cycles" : `No ${filter} cycles`}
</h4> </h4>
<button <button
type="button" type="button"

View File

@ -1,38 +1,62 @@
import { useRouter } from "next/router";
import { FC } from "react"; import { FC } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CyclesList } from "components/cycles"; import { CyclesBoard, CyclesList } from "components/cycles";
import { Loader } from "components/ui";
export interface ICyclesView { export interface ICyclesView {
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
view: "list" | "board" | "gantt"; view: "list" | "board" | "gantt";
workspaceSlug: string;
projectId: string;
} }
export const CyclesView: FC<ICyclesView> = (props) => { export const CyclesView: FC<ICyclesView> = observer((props) => {
const { filter, view } = props; const { filter, view, workspaceSlug, projectId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store // store
const { cycle: cycleStore } = useMobxStore(); const { cycle: cycleStore } = useMobxStore();
// api call to fetch cycles list
useSWR( useSWR(
workspaceSlug && projectId ? `CYCLES_LIST_${projectId}` : null, workspaceSlug && projectId ? `CYCLES_LIST_${projectId}` : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), filter)
: null
); );
if (!projectId) {
return <></>; const cyclesList = cycleStore.cycles?.[projectId]?.[filter];
} console.log("cyclesList", cyclesList);
console.log("cyclesList", cyclesList);
return ( return (
<> <>
{view === "list" && <CyclesList cycles={cycleStore.cycles[projectId?.toString()]} />} {view === "list" && (
{view === "board" && <CyclesList cycles={cycleStore.cycles[projectId?.toString()]} />} <>
{view === "gantt" && <CyclesList cycles={cycleStore.cycles[projectId?.toString()]} />} {cyclesList ? (
<CyclesList cycles={cyclesList} filter={filter} />
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</>
)}
{view === "board" && (
<>
{cyclesList ? (
<CyclesBoard cycles={cyclesList} filter={filter} />
) : (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
<Loader.Item height="200px" />
<Loader.Item height="200px" />
</Loader>
)}
</>
)}
{view === "gantt" && <CyclesList cycles={cyclesList} filter={filter} />}
</> </>
); );
}; });

View File

@ -59,7 +59,7 @@ export const CycleForm: React.FC<Props> = (props) => {
name="name" name="name"
type="text" type="text"
placeholder="Cycle Name" placeholder="Cycle Name"
className="resize-none text-xl" className="resize-none text-xl w-full p-2"
value={value} value={value}
onChange={onChange} onChange={onChange}
hasError={Boolean(errors?.name)} hasError={Boolean(errors?.name)}

View File

@ -13,5 +13,7 @@ export * from "./single-cycle-list";
export * from "./transfer-issues-modal"; export * from "./transfer-issues-modal";
export * from "./transfer-issues"; export * from "./transfer-issues";
export * from "./cycles-list"; export * from "./cycles-list";
export * from "./cycles-list-item";
export * from "./cycles-board"; export * from "./cycles-board";
export * from "./cycles-board-card";
export * from "./cycles-gantt"; export * from "./cycles-gantt";

View File

@ -168,26 +168,46 @@ const ProjectCyclesPage: NextPage = observer(() => {
</div> </div>
<Tab.Panels as={React.Fragment}> <Tab.Panels as={React.Fragment}>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto"> <Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
{cycleTab && cyclesView && ( {cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} /> <CyclesView
filter={cycleTab as ICycleAPIFilter}
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto"> <Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
<ActiveCycleDetails /> <ActiveCycleDetails />
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto"> <Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
{cycleTab && cyclesView && ( {cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} /> <CyclesView
filter={cycleTab as ICycleAPIFilter}
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto"> <Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
{cycleTab && cyclesView && ( {cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} /> <CyclesView
filter={cycleTab as ICycleAPIFilter}
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto"> <Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
{cycleTab && cyclesView && ( {cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} /> <CyclesView
filter={cycleTab as ICycleAPIFilter}
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)} )}
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>

View File

@ -10,12 +10,7 @@ export class CycleService extends APIService {
super(API_BASE_URL); super(API_BASE_URL);
} }
async createCycle( async createCycle(workspaceSlug: string, projectId: string, data: any, user: any): Promise<any> {
workspaceSlug: string,
projectId: string,
data: any,
user: ICurrentUserResponse | undefined
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user); trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);

View File

@ -12,7 +12,9 @@ export interface ICycleStore {
error: any | null; error: any | null;
cycles: { cycles: {
[project_id: string]: ICycle[]; [project_id: string]: {
[filter_name: string]: ICycle[];
};
}; };
cycle_details: { cycle_details: {
@ -21,9 +23,11 @@ export interface ICycleStore {
fetchCycles: ( fetchCycles: (
workspaceSlug: string, workspaceSlug: string,
projectSlug: string, projectId: string,
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
) => Promise<void>; ) => Promise<void>;
createCycle: (workspaceSlug: string, projectId: string, data: any, filter: string) => Promise<ICycle>;
} }
class CycleStore implements ICycleStore { class CycleStore implements ICycleStore {
@ -31,7 +35,9 @@ class CycleStore implements ICycleStore {
error: any | null = null; error: any | null = null;
cycles: { cycles: {
[project_id: string]: ICycle[]; [project_id: string]: {
[filter_name: string]: ICycle[];
};
} = {}; } = {};
cycle_details: { cycle_details: {
@ -56,6 +62,7 @@ class CycleStore implements ICycleStore {
projectCycles: computed, projectCycles: computed,
// actions // actions
fetchCycles: action, fetchCycles: action,
createCycle: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -73,19 +80,21 @@ class CycleStore implements ICycleStore {
// actions // actions
fetchCycles = async ( fetchCycles = async (
workspaceSlug: string, workspaceSlug: string,
projectSlug: string, projectId: string,
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
) => { ) => {
try { try {
this.loader = true; this.loader = true;
this.error = null; this.error = null;
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, params); const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params);
runInAction(() => { runInAction(() => {
this.cycles = { this.cycles = {
...this.cycles, ...this.cycles,
[projectSlug]: cyclesResponse, [projectId]: {
[params]: cyclesResponse,
},
}; };
this.loader = false; this.loader = false;
this.error = null; this.error = null;
@ -96,6 +105,32 @@ class CycleStore implements ICycleStore {
this.error = error; this.error = error;
} }
}; };
createCycle = async (workspaceSlug: string, projectId: string, data: any, filter: string) => {
try {
const response = await this.cycleService.createCycle(
workspaceSlug,
projectId,
data,
this.rootStore.user.currentUser
);
runInAction(() => {
this.cycles = {
...this.cycles,
[projectId]: {
...this.cycles[projectId],
[filter]: [...this.cycles[projectId][filter], response],
},
};
});
return response;
} catch (error) {
console.log("Failed to create cycle from cycle store");
throw error;
}
};
} }
export default CycleStore; export default CycleStore;