forked from github/plane
fix: cycles views list and board
This commit is contained in:
parent
a39aa80e76
commit
9c2ea8a7ae
366
web/components/cycles/cycles-board-card.tsx
Normal file
366
web/components/cycles/cycles-board-card.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
@ -23,12 +23,12 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
|
||||
type TCycledListItem = {
|
||||
type TCyclesListItem = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle: () => void;
|
||||
handleDeleteCycle: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
handleEditCycle?: () => void;
|
||||
handleDeleteCycle?: () => void;
|
||||
handleAddToFavorites?: () => void;
|
||||
handleRemoveFromFavorites?: () => void;
|
||||
};
|
||||
|
||||
const stateGroups = [
|
||||
@ -59,12 +59,12 @@ const stateGroups = [
|
||||
},
|
||||
];
|
||||
|
||||
export const CycledListItem: FC<TCycledListItem> = (props) => {
|
||||
const { cycle, handleEditCycle, handleDeleteCycle, handleAddToFavorites, handleRemoveFromFavorites } = props;
|
||||
export const CyclesListItem: FC<TCyclesListItem> = (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);
|
||||
@ -91,6 +91,11 @@ export const CycledListItem: FC<TCycledListItem> = (props) => {
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const handleAddToFavorites = () => {};
|
||||
const handleRemoveFromFavorites = () => {};
|
||||
const handleEditCycle = () => {};
|
||||
const handleDeleteCycle = () => {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col text-xs hover:bg-custom-background-80">
|
||||
@ -246,33 +251,18 @@ export const CycledListItem: FC<TCycledListItem> = (props) => {
|
||||
</span>
|
||||
</Tooltip>
|
||||
{cycle.is_favorite ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemoveFromFavorites();
|
||||
}}
|
||||
>
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddToFavorites();
|
||||
}}
|
||||
>
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleEditCycle();
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit Cycle</span>
|
||||
@ -280,24 +270,14 @@ export const CycledListItem: FC<TCycledListItem> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteCycle();
|
||||
}}
|
||||
>
|
||||
<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();
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy cycle link</span>
|
||||
|
@ -3,13 +3,15 @@ import { FC } from "react";
|
||||
import { Loader } from "components/ui";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
import { CyclesListItem } from "./cycles-list-item";
|
||||
|
||||
export interface ICyclesList {
|
||||
cycles: ICycle[];
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export const CyclesList: FC<ICyclesList> = (props) => {
|
||||
const { cycles } = props;
|
||||
const { cycles, filter } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -18,16 +20,9 @@ export const CyclesList: FC<ICyclesList> = (props) => {
|
||||
{cycles.length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{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">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
<CyclesListItem cycle={cycle} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -45,7 +40,7 @@ export const CyclesList: FC<ICyclesList> = (props) => {
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-sm text-custom-text-200">
|
||||
{cycleTab === "all" ? "No cycles" : `No ${cycleTab} cycles`}
|
||||
{filter === "all" ? "No cycles" : `No ${filter} cycles`}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -1,38 +1,62 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CyclesList } from "components/cycles";
|
||||
import { CyclesBoard, CyclesList } from "components/cycles";
|
||||
import { Loader } from "components/ui";
|
||||
|
||||
export interface ICyclesView {
|
||||
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
|
||||
view: "list" | "board" | "gantt";
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const CyclesView: FC<ICyclesView> = (props) => {
|
||||
const { filter, view } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
const { filter, view, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
|
||||
// api call to fetch cycles list
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `CYCLES_LIST_${projectId}` : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), filter)
|
||||
: null
|
||||
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
|
||||
);
|
||||
if (!projectId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const cyclesList = cycleStore.cycles?.[projectId]?.[filter];
|
||||
console.log("cyclesList", cyclesList);
|
||||
console.log("cyclesList", cyclesList);
|
||||
|
||||
return (
|
||||
<>
|
||||
{view === "list" && <CyclesList cycles={cycleStore.cycles[projectId?.toString()]} />}
|
||||
{view === "board" && <CyclesList cycles={cycleStore.cycles[projectId?.toString()]} />}
|
||||
{view === "gantt" && <CyclesList cycles={cycleStore.cycles[projectId?.toString()]} />}
|
||||
{view === "list" && (
|
||||
<>
|
||||
{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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -59,7 +59,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Cycle Name"
|
||||
className="resize-none text-xl"
|
||||
className="resize-none text-xl w-full p-2"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors?.name)}
|
||||
|
@ -13,5 +13,7 @@ export * from "./single-cycle-list";
|
||||
export * from "./transfer-issues-modal";
|
||||
export * from "./transfer-issues";
|
||||
export * from "./cycles-list";
|
||||
export * from "./cycles-list-item";
|
||||
export * from "./cycles-board";
|
||||
export * from "./cycles-board-card";
|
||||
export * from "./cycles-gantt";
|
||||
|
@ -168,26 +168,46 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
||||
</div>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
{cycleTab && cyclesView && (
|
||||
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} />
|
||||
{cycleTab && cyclesView && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter={cycleTab as ICycleAPIFilter}
|
||||
view={cyclesView as ICycleView}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
|
||||
<ActiveCycleDetails />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
{cycleTab && cyclesView && (
|
||||
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} />
|
||||
{cycleTab && cyclesView && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter={cycleTab as ICycleAPIFilter}
|
||||
view={cyclesView as ICycleView}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
{cycleTab && cyclesView && (
|
||||
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} />
|
||||
{cycleTab && cyclesView && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter={cycleTab as ICycleAPIFilter}
|
||||
view={cyclesView as ICycleView}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
{cycleTab && cyclesView && (
|
||||
<CyclesView filter={cycleTab as ICycleAPIFilter} view={cyclesView as ICycleView} />
|
||||
{cycleTab && cyclesView && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter={cycleTab as ICycleAPIFilter}
|
||||
view={cyclesView as ICycleView}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
|
@ -10,12 +10,7 @@ export class CycleService extends APIService {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async createCycle(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: any,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async createCycle(workspaceSlug: string, projectId: string, data: any, user: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||
.then((response) => {
|
||||
trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);
|
||||
|
@ -12,7 +12,9 @@ export interface ICycleStore {
|
||||
error: any | null;
|
||||
|
||||
cycles: {
|
||||
[project_id: string]: ICycle[];
|
||||
[project_id: string]: {
|
||||
[filter_name: string]: ICycle[];
|
||||
};
|
||||
};
|
||||
|
||||
cycle_details: {
|
||||
@ -21,9 +23,11 @@ export interface ICycleStore {
|
||||
|
||||
fetchCycles: (
|
||||
workspaceSlug: string,
|
||||
projectSlug: string,
|
||||
projectId: string,
|
||||
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
||||
) => Promise<void>;
|
||||
|
||||
createCycle: (workspaceSlug: string, projectId: string, data: any, filter: string) => Promise<ICycle>;
|
||||
}
|
||||
|
||||
class CycleStore implements ICycleStore {
|
||||
@ -31,7 +35,9 @@ class CycleStore implements ICycleStore {
|
||||
error: any | null = null;
|
||||
|
||||
cycles: {
|
||||
[project_id: string]: ICycle[];
|
||||
[project_id: string]: {
|
||||
[filter_name: string]: ICycle[];
|
||||
};
|
||||
} = {};
|
||||
|
||||
cycle_details: {
|
||||
@ -56,6 +62,7 @@ class CycleStore implements ICycleStore {
|
||||
projectCycles: computed,
|
||||
// actions
|
||||
fetchCycles: action,
|
||||
createCycle: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
@ -73,19 +80,21 @@ class CycleStore implements ICycleStore {
|
||||
// actions
|
||||
fetchCycles = async (
|
||||
workspaceSlug: string,
|
||||
projectSlug: string,
|
||||
projectId: string,
|
||||
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
||||
) => {
|
||||
try {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
|
||||
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, params);
|
||||
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params);
|
||||
|
||||
runInAction(() => {
|
||||
this.cycles = {
|
||||
...this.cycles,
|
||||
[projectSlug]: cyclesResponse,
|
||||
[projectId]: {
|
||||
[params]: cyclesResponse,
|
||||
},
|
||||
};
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
@ -96,6 +105,32 @@ class CycleStore implements ICycleStore {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user