forked from github/plane
fix: cycles layout
This commit is contained in:
parent
394f0bf555
commit
053ebc031d
@ -42,13 +42,7 @@ import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle, IIssue } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_ISSUES,
|
||||
CYCLE_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_CURRENT_LIST, CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
|
@ -1,82 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
// image
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
// icon
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
|
||||
type TCycleStatsViewProps = {
|
||||
cycles: ICycle[] | undefined;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
type: "current" | "upcoming" | "draft";
|
||||
};
|
||||
|
||||
export const AllCyclesBoard: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
type,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||
<h3 className="text-base font-medium text-brand-base ">No cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,86 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleList } from "components/cycles";
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
// image
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
// icon
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
|
||||
type TCycleStatsViewProps = {
|
||||
cycles: ICycle[] | undefined;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
type: "current" | "upcoming" | "draft";
|
||||
};
|
||||
|
||||
export const AllCyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
type,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
<div>
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||
<h3 className="text-base font-medium text-brand-base ">No cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,129 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleCard, SingleCycleList } from "components/cycles";
|
||||
// icons
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys";
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
// image
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
|
||||
export interface CompletedCyclesListProps {
|
||||
cycleView: string;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
}
|
||||
|
||||
export const CompletedCycles: React.FC<CompletedCyclesListProps> = ({
|
||||
cycleView,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: completedCycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, {
|
||||
cycle_view: "completed",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{completedCycles ? (
|
||||
completedCycles.length > 0 ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-brand-secondary">
|
||||
<ExclamationIcon
|
||||
height={14}
|
||||
width={14}
|
||||
className="fill-current text-brand-secondary"
|
||||
/>
|
||||
<span>Completed cycles are not editable.</span>
|
||||
</div>
|
||||
{cycleView === "list" && (
|
||||
<div>
|
||||
{completedCycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
isCompleted
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{cycleView === "board" && (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{completedCycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
isCompleted
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
31
apps/app/components/cycles/cycles-list/all-cycles-list.tsx
Normal file
31
apps/app/components/cycles/cycles-list/all-cycles-list.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { CyclesView } from "components/cycles";
|
||||
// fetch-keys
|
||||
import { CYCLE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: allCyclesList } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_LIST(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), {
|
||||
cycle_view: "all",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
return <CyclesView cycles={allCyclesList} viewType={viewType} />;
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { CyclesView } from "components/cycles";
|
||||
// fetch-keys
|
||||
import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const CompletedCyclesList: React.FC<Props> = ({ viewType }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: completedCyclesList } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), {
|
||||
cycle_view: "completed",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
return <CyclesView cycles={completedCyclesList} viewType={viewType} />;
|
||||
};
|
31
apps/app/components/cycles/cycles-list/draft-cycles-list.tsx
Normal file
31
apps/app/components/cycles/cycles-list/draft-cycles-list.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { CyclesView } from "components/cycles";
|
||||
// fetch-keys
|
||||
import { CYCLE_DRAFT_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const DraftCyclesList: React.FC<Props> = ({ viewType }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: draftCyclesList } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), {
|
||||
cycle_view: "draft",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
return <CyclesView cycles={draftCyclesList} viewType={viewType} />;
|
||||
};
|
4
apps/app/components/cycles/cycles-list/index.ts
Normal file
4
apps/app/components/cycles/cycles-list/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./all-cycles-list";
|
||||
export * from "./completed-cycles-list";
|
||||
export * from "./draft-cycles-list";
|
||||
export * from "./upcoming-cycles-list";
|
@ -0,0 +1,31 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { CyclesView } from "components/cycles";
|
||||
// fetch-keys
|
||||
import { CYCLE_UPCOMING_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const UpcomingCyclesList: React.FC<Props> = ({ viewType }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: upcomingCyclesList } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), {
|
||||
cycle_view: "upcoming",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
return <CyclesView cycles={upcomingCyclesList} viewType={viewType} />;
|
||||
};
|
@ -1,246 +1,229 @@
|
||||
import React, { useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
ActiveCycleDetails,
|
||||
CompletedCyclesListProps,
|
||||
AllCyclesBoard,
|
||||
AllCyclesList,
|
||||
CreateUpdateCycleModal,
|
||||
CyclesListGanttChartView,
|
||||
DeleteCycleModal,
|
||||
SingleCycleCard,
|
||||
SingleCycleList,
|
||||
} from "components/cycles";
|
||||
// ui
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
// icons
|
||||
import { ChartBarIcon, ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
// images
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
// helpers
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { SelectCycleType, ICycle } from "types";
|
||||
import { ICycle } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_LIST,
|
||||
CYCLE_UPCOMING_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
cyclesCompleteList: ICycle[] | undefined;
|
||||
currentCycle: ICycle[] | undefined;
|
||||
upcomingCycles: ICycle[] | undefined;
|
||||
draftCycles: ICycle[] | undefined;
|
||||
cycles: ICycle[] | undefined;
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const CyclesView: React.FC<Props> = ({
|
||||
setSelectedCycle,
|
||||
setCreateUpdateCycleModal,
|
||||
cyclesCompleteList,
|
||||
currentCycle,
|
||||
upcomingCycles,
|
||||
draftCycles,
|
||||
}) => {
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
|
||||
const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list");
|
||||
export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "All":
|
||||
return 0;
|
||||
case "Active":
|
||||
return 1;
|
||||
case "Upcoming":
|
||||
return 2;
|
||||
case "Completed":
|
||||
return 3;
|
||||
case "Drafts":
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
|
||||
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleToUpdate(cycle);
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
const CompletedCycles = dynamic<CompletedCyclesListProps>(
|
||||
() => import("components/cycles").then((a) => a.CompletedCycles),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Loader className="mb-5">
|
||||
<Loader.Item height="12rem" width="100%" />
|
||||
</Loader>
|
||||
),
|
||||
}
|
||||
);
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleToDelete(cycle);
|
||||
setDeleteCycleModal(true);
|
||||
};
|
||||
|
||||
const handleAddToFavorites = (cycle: ICycle) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CYCLE_CURRENT_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? CYCLE_UPCOMING_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? CYCLE_COMPLETE_LIST(projectId as string)
|
||||
: CYCLE_DRAFT_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLE_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (cycle: ICycle) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CYCLE_CURRENT_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? CYCLE_UPCOMING_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? CYCLE_COMPLETE_LIST(projectId as string)
|
||||
: CYCLE_DRAFT_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLE_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "list" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setCyclesView("list")}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "board" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setCyclesView("board")}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "gantt_chart" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCyclesView("gantt_chart");
|
||||
setCycleTab("All");
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-rounded text-brand-secondary text-[18px] rotate-90">
|
||||
waterfall_chart
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group
|
||||
as={React.Fragment}
|
||||
defaultIndex={currentTabValue(cycleTab)}
|
||||
selectedIndex={currentTabValue(cycleTab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setCycleTab("All");
|
||||
case 1:
|
||||
return setCycleTab("Active");
|
||||
case 2:
|
||||
return setCycleTab("Upcoming");
|
||||
case 3:
|
||||
return setCycleTab("Completed");
|
||||
case 4:
|
||||
return setCycleTab("Drafts");
|
||||
default:
|
||||
return setCycleTab("All");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
||||
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => {
|
||||
if (
|
||||
cyclesView === "gantt_chart" &&
|
||||
(tab === "Active" || tab === "Drafts" || tab === "Completed")
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-6 py-1 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
||||
{cyclesView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={cyclesCompleteList}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={cyclesCompleteList}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "gantt_chart" && (
|
||||
<CyclesListGanttChartView cycles={cyclesCompleteList ?? []} />
|
||||
)}
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{currentCycle?.[0] ? (
|
||||
<ActiveCycleDetails cycle={currentCycle?.[0]} />
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
<CreateUpdateCycleModal
|
||||
isOpen={createUpdateCycleModal}
|
||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||
data={selectedCycleToUpdate}
|
||||
/>
|
||||
<DeleteCycleModal
|
||||
isOpen={deleteCycleModal}
|
||||
setIsOpen={setDeleteCycleModal}
|
||||
data={selectedCycleToDelete}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
viewType === "list" ? (
|
||||
<div className="divide-y divide-brand-base">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : viewType === "board" ? (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
||||
{cyclesView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={upcomingCycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={upcomingCycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "gantt_chart" && (
|
||||
<CyclesListGanttChartView cycles={upcomingCycles ?? []} />
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
<CompletedCycles
|
||||
cycleView={cyclesView ?? "list"}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{cyclesView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={draftCycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
)}
|
||||
{cyclesView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={draftCycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CyclesListGanttChartView cycles={cycles ?? []} />
|
||||
)
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : viewType === "list" ? (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
) : viewType === "board" ? (
|
||||
<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>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="300px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ import type { ICycle } from "types";
|
||||
type TConfirmCycleDeletionProps = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
data?: ICycle;
|
||||
data?: ICycle | null;
|
||||
};
|
||||
// fetch-keys
|
||||
import {
|
||||
|
@ -1,78 +0,0 @@
|
||||
import React from "react";
|
||||
import { LinearProgressIndicator } from "components/ui";
|
||||
|
||||
export const EmptyCycle = () => {
|
||||
const emptyCycleData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "backlog",
|
||||
value: 20,
|
||||
color: "#DEE2E6",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "unstarted",
|
||||
value: 14,
|
||||
color: "#26B5CE",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "started",
|
||||
value: 27,
|
||||
color: "#F7AE59",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "cancelled",
|
||||
value: 15,
|
||||
color: "#D687FF",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "completed",
|
||||
value: 14,
|
||||
color: "#09A953",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-5 ">
|
||||
<div className="relative h-32 w-72">
|
||||
<div className="absolute right-0 top-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
||||
<span className="text-sm font-semibold text-brand-base">Cycle Name</span>
|
||||
<div className="flex h-full w-full items-center gap-4">
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
|
||||
<LinearProgressIndicator data={emptyCycleData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-0 bottom-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
||||
<span className="text-sm font-semibold text-brand-base">Cycle Name</span>
|
||||
<div className="flex h-full w-full items-center gap-4">
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
|
||||
<LinearProgressIndicator data={emptyCycleData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-center ">
|
||||
<h3 className="text-xl font-semibold">Create New Cycle</h3>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
Sprint more effectively with Cycles by confining your project <br /> to a fixed amount of
|
||||
time. Create new cycle now.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,18 +1,15 @@
|
||||
export * from "./cycles-list";
|
||||
export * from "./active-cycle-details";
|
||||
export * from "./cycles-view";
|
||||
export * from "./completed-cycles";
|
||||
export * from "./active-cycle-stats";
|
||||
export * from "./cycles-list-gantt-chart";
|
||||
export * from "./all-cycles-board";
|
||||
export * from "./all-cycles-list";
|
||||
export * from "./cycles-view";
|
||||
export * from "./delete-cycle-modal";
|
||||
export * from "./form";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./modal";
|
||||
export * from "./select";
|
||||
export * from "./sidebar";
|
||||
export * from "./single-cycle-list";
|
||||
export * from "./single-cycle-card";
|
||||
export * from "./empty-cycle";
|
||||
export * from "./single-cycle-list";
|
||||
export * from "./transfer-issues-modal";
|
||||
export * from "./transfer-issues";
|
||||
export * from "./active-cycle-stats";
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
type CycleModalProps = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: ICycle;
|
||||
data?: ICycle | null;
|
||||
};
|
||||
|
||||
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
|
@ -4,20 +4,17 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// headless ui
|
||||
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 { Disclosure, Transition } from "@headlessui/react";
|
||||
import { AssigneesList, Avatar } from "components/ui/avatar";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
// icons
|
||||
import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
@ -42,19 +39,13 @@ import {
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_LIST,
|
||||
CYCLE_UPCOMING_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle: () => void;
|
||||
handleDeleteCycle: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
@ -90,6 +81,8 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
cycle,
|
||||
handleEditCycle,
|
||||
handleDeleteCycle,
|
||||
handleAddToFavorites,
|
||||
handleRemoveFromFavorites,
|
||||
isCompleted = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -101,94 +94,6 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CYCLE_CURRENT_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? CYCLE_UPCOMING_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? CYCLE_COMPLETE_LIST(projectId as string)
|
||||
: CYCLE_DRAFT_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLE_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CYCLE_CURRENT_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? CYCLE_UPCOMING_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? CYCLE_COMPLETE_LIST(projectId as string)
|
||||
: CYCLE_DRAFT_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLE_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
@ -4,17 +4,12 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
|
||||
// icons
|
||||
import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
@ -33,20 +28,13 @@ import {
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_LIST,
|
||||
CYCLE_UPCOMING_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { type } from "os";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle: () => void;
|
||||
handleDeleteCycle: () => void;
|
||||
handleAddToFavorites: () => void;
|
||||
handleRemoveFromFavorites: () => void;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
@ -124,6 +112,8 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
cycle,
|
||||
handleEditCycle,
|
||||
handleDeleteCycle,
|
||||
handleAddToFavorites,
|
||||
handleRemoveFromFavorites,
|
||||
isCompleted = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -135,94 +125,6 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CYCLE_CURRENT_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? CYCLE_UPCOMING_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? CYCLE_COMPLETE_LIST(projectId as string)
|
||||
: CYCLE_DRAFT_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLE_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CYCLE_CURRENT_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? CYCLE_UPCOMING_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? CYCLE_COMPLETE_LIST(projectId as string)
|
||||
: CYCLE_DRAFT_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLE_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
@ -250,7 +152,7 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col border-b border-brand-base text-xs hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col text-xs hover:bg-brand-surface-2">
|
||||
<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">
|
||||
|
@ -3,92 +3,46 @@ import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// services
|
||||
import cycleService from "services/cycles.service";
|
||||
import projectService from "services/project.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { CreateUpdateCycleModal, CyclesView } from "components/cycles";
|
||||
import {
|
||||
ActiveCycleDetails,
|
||||
AllCyclesList,
|
||||
CompletedCyclesList,
|
||||
CreateUpdateCycleModal,
|
||||
DraftCyclesList,
|
||||
UpcomingCyclesList,
|
||||
} from "components/cycles";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
import { EmptyState, PrimaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
// images
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
// types
|
||||
import { SelectCycleType } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DRAFT_LIST,
|
||||
PROJECT_DETAILS,
|
||||
CYCLE_UPCOMING_LIST,
|
||||
CYCLE_CURRENT_LIST,
|
||||
CYCLE_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { PROJECT_DETAILS, CYCLE_CURRENT_LIST } from "constants/fetch-keys";
|
||||
|
||||
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
|
||||
|
||||
const ProjectCycles: NextPage = () => {
|
||||
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: activeProject } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: draftCycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, {
|
||||
cycle_view: "draft",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: currentCycle } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, {
|
||||
cycle_view: "current",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: upcomingCycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, {
|
||||
cycle_view: "upcoming",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cyclesCompleteList } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, {
|
||||
cycle_view: "all",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (createUpdateCycleModal) return;
|
||||
const timer = setTimeout(() => {
|
||||
setSelectedCycle(undefined);
|
||||
clearTimeout(timer);
|
||||
}, 500);
|
||||
}, [createUpdateCycleModal]);
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
|
||||
const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list");
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
@ -107,6 +61,34 @@ const ProjectCycles: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: activeProject } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: currentCycle } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, {
|
||||
cycle_view: "current",
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (createUpdateCycleModal) return;
|
||||
const timer = setTimeout(() => {
|
||||
setSelectedCycle(undefined);
|
||||
clearTimeout(timer);
|
||||
}, 500);
|
||||
}, [createUpdateCycleModal]);
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
meta={{
|
||||
@ -137,14 +119,116 @@ const ProjectCycles: NextPage = () => {
|
||||
data={selectedCycle}
|
||||
/>
|
||||
<div className="space-y-5 p-8 h-full flex flex-col overflow-hidden">
|
||||
<CyclesView
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
cyclesCompleteList={cyclesCompleteList}
|
||||
currentCycle={currentCycle}
|
||||
upcomingCycles={upcomingCycles}
|
||||
draftCycles={draftCycles}
|
||||
/>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "list" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setCyclesView("list")}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "board" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setCyclesView("board")}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
cyclesView === "gantt_chart" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCyclesView("gantt_chart");
|
||||
setCycleTab("All");
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-rounded text-brand-secondary text-[18px] rotate-90">
|
||||
waterfall_chart
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group
|
||||
as={React.Fragment}
|
||||
defaultIndex={currentTabValue(cycleTab)}
|
||||
selectedIndex={currentTabValue(cycleTab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setCycleTab("All");
|
||||
case 1:
|
||||
return setCycleTab("Active");
|
||||
case 2:
|
||||
return setCycleTab("Upcoming");
|
||||
case 3:
|
||||
return setCycleTab("Completed");
|
||||
case 4:
|
||||
return setCycleTab("Drafts");
|
||||
default:
|
||||
return setCycleTab("All");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
||||
{tabsList.map((tab, index) => {
|
||||
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-6 py-1 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</Tab.List>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
<AllCyclesList viewType={cyclesView} />
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{currentCycle?.[0] ? (
|
||||
<ActiveCycleDetails cycle={currentCycle?.[0]} />
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
<UpcomingCyclesList viewType={cyclesView} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
<CompletedCyclesList viewType={cyclesView} />
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
<DraftCyclesList viewType={cyclesView} />
|
||||
</Tab.Panel>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user