chore: update cycle store structure

This commit is contained in:
Aaryan Khandelwal 2023-12-15 11:43:36 +05:30
parent 8521a54e49
commit 019e2e4356
19 changed files with 507 additions and 482 deletions

View File

@ -4,8 +4,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useApplication, useCycle } from "hooks/store";
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { SingleProgressStats } from "components/core"; import { SingleProgressStats } from "components/core";
@ -71,20 +70,20 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = props; const { workspaceSlug, projectId } = props;
// store hooks // store hooks
const { cycle: cycleStore } = useMobxStore();
const { const {
commandPalette: { toggleCreateCycleModal }, commandPalette: { toggleCreateCycleModal },
} = useApplication(); } = useApplication();
const { fetchActiveCycle, projectActiveCycle, getActiveCycleById, addCycleToFavorites, removeCycleFromFavorites } =
useCycle();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { isLoading } = useSWR( const { isLoading } = useSWR(
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
); );
const activeCycle = cycleStore.cycles?.[projectId]?.current || null; const activeCycle = projectActiveCycle ? getActiveCycleById(projectActiveCycle) : null;
const cycle = activeCycle ? activeCycle[0] : null;
const issues = (cycleStore?.active_cycle_issues as any) || null; const issues = (cycleStore?.active_cycle_issues as any) || null;
// const { data: issues } = useSWR( // const { data: issues } = useSWR(
@ -97,14 +96,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// : null // : null
// ) as { data: IIssue[] | undefined }; // ) as { data: IIssue[] | undefined };
if (!cycle && isLoading) if (!activeCycle && isLoading)
return ( return (
<Loader> <Loader>
<Loader.Item height="250px" /> <Loader.Item height="250px" />
</Loader> </Loader>
); );
if (!cycle) if (!activeCycle)
return ( return (
<div className="grid h-full place-items-center text-center"> <div className="grid h-full place-items-center text-center">
<div className="space-y-2"> <div className="space-y-2">
@ -129,24 +128,24 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div> </div>
); );
const endDate = new Date(cycle.end_date ?? ""); const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? ""); const startDate = new Date(activeCycle.start_date ?? "");
const groupedIssues: any = { const groupedIssues: any = {
backlog: cycle.backlog_issues, backlog: activeCycle.backlog_issues,
unstarted: cycle.unstarted_issues, unstarted: activeCycle.unstarted_issues,
started: cycle.started_issues, started: activeCycle.started_issues,
completed: cycle.completed_issues, completed: activeCycle.completed_issues,
cancelled: cycle.cancelled_issues, cancelled: activeCycle.cancelled_issues,
}; };
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); const cycleStatus = getDateRangeStatus(activeCycle.start_date, activeCycle.end_date);
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => { const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -159,7 +158,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -171,7 +170,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const progressIndicatorData = stateGroups.map((group, index) => ({ const progressIndicatorData = stateGroups.map((group, index) => ({
id: index, id: index,
name: group.title, name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, value:
activeCycle.total_issues > 0
? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100
: 0,
color: group.color, color: group.color,
})); }));
@ -199,8 +201,8 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
}`} }`}
/> />
</span> </span>
<Tooltip tooltipContent={cycle.name} position="top-left"> <Tooltip tooltipContent={activeCycle.name} position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 70)}</h3> <h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
</Tooltip> </Tooltip>
</span> </span>
<span className="flex items-center gap-1 capitalize"> <span className="flex items-center gap-1 capitalize">
@ -221,19 +223,19 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{cycleStatus === "current" ? ( {cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap"> <span className="flex gap-1 whitespace-nowrap">
<RunningIcon className="h-4 w-4" /> <RunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left
</span> </span>
) : cycleStatus === "upcoming" ? ( ) : cycleStatus === "upcoming" ? (
<span className="flex gap-1 whitespace-nowrap"> <span className="flex gap-1 whitespace-nowrap">
<AlarmClock className="h-4 w-4" /> <AlarmClock className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left
</span> </span>
) : cycleStatus === "completed" ? ( ) : cycleStatus === "completed" ? (
<span className="flex gap-1 whitespace-nowrap"> <span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues - cycle.completed_issues > 0 && ( {activeCycle.total_issues - activeCycle.completed_issues > 0 && (
<Tooltip <Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${ tooltipContent={`${activeCycle.total_issues - activeCycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues" activeCycle.total_issues - activeCycle.completed_issues === 1 ? "issue" : "issues"
}`} }`}
> >
<span> <span>
@ -247,7 +249,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus cycleStatus
)} )}
</span> </span>
{cycle.is_favorite ? ( {activeCycle.is_favorite ? (
<button <button
onClick={(e) => { onClick={(e) => {
handleRemoveFromFavorites(e); handleRemoveFromFavorites(e);
@ -281,26 +283,26 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2.5 text-custom-text-200"> <div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( {activeCycle.owned_by.avatar && activeCycle.owned_by.avatar !== "" ? (
<img <img
src={cycle.owned_by.avatar} src={activeCycle.owned_by.avatar}
height={16} height={16}
width={16} width={16}
className="rounded-full" className="rounded-full"
alt={cycle.owned_by.display_name} alt={activeCycle.owned_by.display_name}
/> />
) : ( ) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize"> <span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.display_name.charAt(0)} {activeCycle.owned_by.display_name.charAt(0)}
</span> </span>
)} )}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span> <span className="text-custom-text-200">{activeCycle.owned_by.display_name}</span>
</div> </div>
{cycle.assignees.length > 0 && ( {activeCycle.assignees.length > 0 && (
<div className="flex items-center gap-1 text-custom-text-200"> <div className="flex items-center gap-1 text-custom-text-200">
<AvatarGroup> <AvatarGroup>
{cycle.assignees.map((assignee) => ( {activeCycle.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} /> <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))} ))}
</AvatarGroup> </AvatarGroup>
@ -311,15 +313,15 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4 text-custom-text-200"> <div className="flex items-center gap-4 text-custom-text-200">
<div className="flex gap-2"> <div className="flex gap-2">
<LayersIcon className="h-4 w-4 flex-shrink-0" /> <LayersIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues {activeCycle.total_issues} issues
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StateGroupIcon stateGroup="completed" height="14px" width="14px" /> <StateGroupIcon stateGroup="completed" height="14px" width="14px" />
{cycle.completed_issues} issues {activeCycle.completed_issues} issues
</div> </div>
</div> </div>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${activeCycle.id}`}>
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90"> <span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
View Cycle View Cycle
</span> </span>
@ -350,14 +352,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div> </div>
} }
completed={groupedIssues[group]} completed={groupedIssues[group]}
total={cycle.total_issues} total={activeCycle.total_issues}
/> />
))} ))}
</div> </div>
</div> </div>
</div> </div>
<div className="h-60 overflow-y-scroll border-custom-border-200"> <div className="h-60 overflow-y-scroll border-custom-border-200">
<ActiveCycleProgressStats cycle={cycle} /> <ActiveCycleProgressStats cycle={activeCycle} />
</div> </div>
</div> </div>
</div> </div>
@ -469,15 +471,18 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<span> <span>
<LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" /> <LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" />
</span> </span>
<span>Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)}</span> <span>
Pending Issues -{" "}
{activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
</span>
</div> </div>
</div> </div>
<div className="relative h-64"> <div className="relative h-64">
<ProgressChart <ProgressChart
distribution={cycle.distribution?.completion_chart ?? {}} distribution={activeCycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""} startDate={activeCycle.start_date ?? ""}
endDate={cycle.end_date ?? ""} endDate={activeCycle.end_date ?? ""}
totalIssues={cycle.total_issues} totalIssues={activeCycle.total_issues}
/> />
</div> </div>
</div> </div>

View File

@ -1,10 +1,8 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; // hooks
import { useCycle } from "hooks/store";
// components // components
import { CycleDetailsSidebar } from "./sidebar"; import { CycleDetailsSidebar } from "./sidebar";
@ -14,14 +12,13 @@ type Props = {
}; };
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => { export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
// router
const router = useRouter(); const router = useRouter();
const { peekCycle } = router.query; const { peekCycle } = router.query;
// refs
const ref = React.useRef(null); const ref = React.useRef(null);
// store hooks
const { cycle: cycleStore } = useMobxStore(); const { fetchCycleDetails } = useCycle();
const { fetchCycleWithId } = cycleStore;
const handleClose = () => { const handleClose = () => {
delete router.query.peekCycle; delete router.query.peekCycle;
@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
useEffect(() => { useEffect(() => {
if (!peekCycle) return; if (!peekCycle) return;
fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString()); fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
}, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]); }, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]);
return ( return (
<> <>

View File

@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// hooks // hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
@ -17,10 +18,6 @@ import {
renderShortMonthDate, renderShortMonthDate,
} from "helpers/date-time.helper"; } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// constants // constants
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
@ -28,61 +25,33 @@ import { EUserWorkspaceRoles } from "constants/workspace";
export interface ICyclesBoardCard { export interface ICyclesBoardCard {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
cycle: ICycle; cycleId: string;
} }
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => { export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const { cycle, workspaceSlug, projectId } = props; const { cycleId, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { setTrackElement },
user: userStore,
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states // states
const [updateModal, setUpdateModal] = useState(false); const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
// computed // router
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 isDateValid = cycle.start_date || cycle.end_date;
const { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const router = useRouter(); const router = useRouter();
// store
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const {
eventTracker: { setTrackElement },
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); } = useApplication();
const {
const cycleTotalIssues = membership: { currentProjectRole },
cycle.backlog_issues + } = useUser();
cycle.unstarted_issues + const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
cycle.started_issues + // toast alert
cycle.completed_issues + const { setToastAlert } = useToast();
cycle.cancelled_issues;
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycle
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycle.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => { const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Link Copied!", title: "Link Copied!",
@ -95,7 +64,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -108,7 +77,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -137,14 +106,48 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekCycle: cycle.id }, query: { ...query, peekCycle: cycleId },
}); });
}; };
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
const cycleStatus = getDateRangeStatus(cycleDetails.start_date, cycleDetails.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const isDateValid = cycleDetails.start_date || cycleDetails.end_date;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycleDetails
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycleDetails.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
return ( return (
<div> <div>
<CycleCreateUpdateModal <CycleCreateUpdateModal
data={cycle} data={cycleDetails}
isOpen={updateModal} isOpen={updateModal}
handleClose={() => setUpdateModal(false)} handleClose={() => setUpdateModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
@ -152,22 +155,22 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
/> />
<CycleDeleteModal <CycleDeleteModal
cycle={cycle} cycle={cycleDetails}
isOpen={deleteModal} isOpen={deleteModal}
handleClose={() => setDeleteModal(false)} handleClose={() => setDeleteModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md"> <div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 truncate"> <div className="flex items-center gap-3 truncate">
<span className="flex-shrink-0"> <span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span> </span>
<Tooltip tooltipContent={cycle.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycle.name}</span> <span className="truncate text-base font-medium">{cycleDetails.name}</span>
</Tooltip> </Tooltip>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -180,7 +183,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
}} }}
> >
{currentCycle.value === "current" {currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`} : `${currentCycle.label}`}
</span> </span>
)} )}
@ -196,11 +199,11 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
<LayersIcon className="h-4 w-4 text-custom-text-300" /> <LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount}</span> <span className="text-xs text-custom-text-300">{issueCount}</span>
</div> </div>
{cycle.assignees.length > 0 && ( {cycleDetails.assignees.length > 0 && (
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex cursor-default items-center gap-1"> <div className="flex cursor-default items-center gap-1">
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycle.assignees.map((assignee) => ( {cycleDetails.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} /> <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))} ))}
</AvatarGroup> </AvatarGroup>
@ -241,7 +244,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
)} )}
<div className="z-10 flex items-center gap-1.5"> <div className="z-10 flex items-center gap-1.5">
{isEditingAllowed && {isEditingAllowed &&
(cycle.is_favorite ? ( (cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}> <button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" /> <Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button> </button>

View File

@ -4,11 +4,9 @@ import { observer } from "mobx-react-lite";
import { useApplication } from "hooks/store"; import { useApplication } from "hooks/store";
// components // components
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
// types
import { ICycle } from "types";
export interface ICyclesBoard { export interface ICyclesBoard {
cycles: ICycle[]; cycleIds: string[];
filter: string; filter: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
@ -16,13 +14,13 @@ export interface ICyclesBoard {
} }
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => { export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
return ( return (
<> <>
{cycles.length > 0 ? ( {cycleIds?.length > 0 ? (
<div className="h-full w-full"> <div className="h-full w-full">
<div className="flex h-full w-full justify-between"> <div className="flex h-full w-full justify-between">
<div <div
@ -32,8 +30,8 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
} auto-rows-max transition-all `} } auto-rows-max transition-all `}
> >
{cycles.map((cycle) => ( {cycleIds.map((cycleId) => (
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} /> <CyclesBoardCard key={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
))} ))}
</div> </div>
<CyclePeekOverview <CyclePeekOverview

View File

@ -1,10 +1,8 @@
import { FC, MouseEvent, useState } from "react"; import { FC, MouseEvent, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// stores
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
@ -20,14 +18,12 @@ import {
renderShortMonthDate, renderShortMonthDate,
} from "helpers/date-time.helper"; } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// constants // constants
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
type TCyclesListItem = { type TCyclesListItem = {
cycle: ICycle; cycleId: string;
handleEditCycle?: () => void; handleEditCycle?: () => void;
handleDeleteCycle?: () => void; handleDeleteCycle?: () => void;
handleAddToFavorites?: () => void; handleAddToFavorites?: () => void;
@ -37,52 +33,29 @@ type TCyclesListItem = {
}; };
export const CyclesListItem: FC<TCyclesListItem> = (props) => { export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle, workspaceSlug, projectId } = props; const { cycleId, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { setTrackElement },
user: userStore,
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states // states
const [updateModal, setUpdateModal] = useState(false); const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
// computed // router
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 { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const router = useRouter(); const router = useRouter();
// store hooks
const cycleTotalIssues = const {
cycle.backlog_issues + eventTracker: { setTrackElement },
cycle.unstarted_issues + } = useApplication();
cycle.started_issues + const {
cycle.completed_issues + membership: { currentProjectRole },
cycle.cancelled_issues; } = useUser();
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const renderDate = cycle.start_date || cycle.end_date; // toast alert
const { setToastAlert } = useToast();
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => { const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Link Copied!", title: "Link Copied!",
@ -95,7 +68,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -108,7 +81,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -137,27 +110,56 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekCycle: cycle.id }, query: { ...query, peekCycle: cycleId },
}); });
}; };
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
const cycleStatus = getDateRangeStatus(cycleDetails.start_date, cycleDetails.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const renderDate = cycleDetails.start_date || cycleDetails.end_date;
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
return ( return (
<> <>
<CycleCreateUpdateModal <CycleCreateUpdateModal
data={cycle} data={cycleDetails}
isOpen={updateModal} isOpen={updateModal}
handleClose={() => setUpdateModal(false)} handleClose={() => setUpdateModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
<CycleDeleteModal <CycleDeleteModal
cycle={cycle} cycle={cycleDetails}
isOpen={deleteModal} isOpen={deleteModal}
handleClose={() => setDeleteModal(false)} handleClose={() => setDeleteModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90"> <div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90">
<div className="flex w-full items-center gap-3 truncate"> <div className="flex w-full items-center gap-3 truncate">
<div className="flex items-center gap-4 truncate"> <div className="flex items-center gap-4 truncate">
@ -181,8 +183,8 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<span className="flex-shrink-0"> <span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span> </span>
<Tooltip tooltipContent={cycle.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycle.name}</span> <span className="truncate text-base font-medium">{cycleDetails.name}</span>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@ -202,7 +204,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
}} }}
> >
{currentCycle.value === "current" {currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`} : `${currentCycle.label}`}
</span> </span>
)} )}
@ -216,11 +218,11 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</span> </span>
)} )}
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex w-16 cursor-default items-center justify-center gap-1"> <div className="flex w-16 cursor-default items-center justify-center gap-1">
{cycle.assignees.length > 0 ? ( {cycleDetails.assignees.length > 0 ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycle.assignees.map((assignee) => ( {cycleDetails.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} /> <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))} ))}
</AvatarGroup> </AvatarGroup>
@ -232,7 +234,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</div> </div>
</Tooltip> </Tooltip>
{isEditingAllowed && {isEditingAllowed &&
(cycle.is_favorite ? ( (cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}> <button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" /> <Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button> </button>

View File

@ -6,18 +6,16 @@ import { useApplication } from "hooks/store";
import { CyclePeekOverview, CyclesListItem } from "components/cycles"; import { CyclePeekOverview, CyclesListItem } from "components/cycles";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types
import { ICycle } from "types";
export interface ICyclesList { export interface ICyclesList {
cycles: ICycle[]; cycleIds: string[];
filter: string; filter: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
} }
export const CyclesList: FC<ICyclesList> = observer((props) => { export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId } = props; const { cycleIds, filter, workspaceSlug, projectId } = props;
// store hooks // store hooks
const { const {
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
@ -26,14 +24,14 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
return ( return (
<> <>
{cycles ? ( {cycleIds ? (
<> <>
{cycles.length > 0 ? ( {cycleIds.length > 0 ? (
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
<div className="flex h-full w-full justify-between"> <div className="flex h-full w-full justify-between">
<div className="flex h-full w-full flex-col overflow-y-auto"> <div className="flex h-full w-full flex-col overflow-y-auto">
{cycles.map((cycle) => ( {cycleIds.map((cycleId) => (
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} /> <CyclesListItem cycleId={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} />
))} ))}
</div> </div>
<CyclePeekOverview <CyclePeekOverview

View File

@ -1,17 +1,16 @@
import { FC } from "react"; import { FC } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useCycle } from "hooks/store";
// components // components
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
// ui components // ui components
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types // types
import { TCycleLayout } from "types"; import { TCycleLayout, TCycleView } from "types";
export interface ICyclesView { export interface ICyclesView {
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; filter: TCycleView;
layout: TCycleLayout; layout: TCycleLayout;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
@ -20,31 +19,24 @@ export interface ICyclesView {
export const CyclesView: FC<ICyclesView> = observer((props) => { export const CyclesView: FC<ICyclesView> = observer((props) => {
const { filter, layout, workspaceSlug, projectId, peekCycle } = props; const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
// store hooks
// store const { projectCompletedCycles, projectDraftCycles, projectUpcomingCycles, projectAllCycles } = useCycle();
const { cycle: cycleStore } = useMobxStore();
// api call to fetch cycles list
useSWR(
workspaceSlug && projectId && filter ? `CYCLES_LIST_${projectId}_${filter}` : null,
workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
);
const cyclesList = const cyclesList =
filter === "completed" filter === "completed"
? cycleStore.projectCompletedCycles ? projectCompletedCycles
: filter === "draft" : filter === "draft"
? cycleStore.projectDraftCycles ? projectDraftCycles
: filter === "upcoming" : filter === "upcoming"
? cycleStore.projectUpcomingCycles ? projectUpcomingCycles
: cycleStore.projectCycles; : projectAllCycles;
return ( return (
<> <>
{layout === "list" && ( {layout === "list" && (
<> <>
{cyclesList ? ( {cyclesList ? (
<CyclesList cycles={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} /> <CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
) : ( ) : (
<Loader className="space-y-4 p-8"> <Loader className="space-y-4 p-8">
<Loader.Item height="50px" /> <Loader.Item height="50px" />
@ -59,7 +51,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
<> <>
{cyclesList ? ( {cyclesList ? (
<CyclesBoard <CyclesBoard
cycles={cyclesList} cycleIds={cyclesList}
filter={filter} filter={filter}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -78,7 +70,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
{layout === "gantt" && ( {layout === "gantt" && (
<> <>
{cyclesList ? ( {cyclesList ? (
<CyclesListGanttChartView cycles={cyclesList} workspaceSlug={workspaceSlug} /> <CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
) : ( ) : (
<Loader className="space-y-4"> <Loader className="space-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" />

View File

@ -1,17 +1,15 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
// next
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast";
// components // components
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
interface ICycleDelete { interface ICycleDelete {
cycle: ICycle; cycle: ICycle;
@ -23,24 +21,25 @@ interface ICycleDelete {
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => { export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { postHogEventTracker },
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states // states
const [loader, setLoader] = useState(false); const [loader, setLoader] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { cycleId, peekCycle } = router.query; const { cycleId, peekCycle } = router.query;
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { deleteCycle } = useCycle();
// toast alert
const { setToastAlert } = useToast();
const formSubmit = async () => { const formSubmit = async () => {
if (!cycle) return;
setLoader(true); setLoader(true);
if (cycle?.id)
try { try {
await cycleStore await deleteCycle(workspaceSlug, projectId, cycle.id)
.removeCycle(workspaceSlug, projectId, cycle?.id)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -67,12 +66,6 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
message: "Something went wrong please try again later.", message: "Something went wrong please try again later.",
}); });
} }
else
setToastAlert({
type: "error",
title: "Warning!",
message: "Something went wrong please try again later.",
});
setLoader(false); setLoader(false);
}; };

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { KeyedMutator } from "swr"; import { KeyedMutator } from "swr";
// hooks // hooks
import { useUser } from "hooks/store"; import { useCycle, useUser } from "hooks/store";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// components // components
@ -16,7 +16,7 @@ import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
cycles: ICycle[]; cycleIds: string[];
mutateCycles?: KeyedMutator<ICycle[]>; mutateCycles?: KeyedMutator<ICycle[]>;
}; };
@ -24,7 +24,7 @@ type Props = {
const cycleService = new CycleService(); const cycleService = new CycleService();
export const CyclesListGanttChartView: FC<Props> = observer((props) => { export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { cycles, mutateCycles } = props; const { cycleIds, mutateCycles } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -32,6 +32,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { getCycleById } = useCycle();
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -65,18 +66,21 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload); cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
}; };
const blockFormat = (blocks: ICycle[]) => const blockFormat = (blocks: (ICycle | null)[]) => {
blocks && blocks.length > 0 if (!blocks) return [];
? blocks
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)) const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date);
.map((block) => ({
const structuredBlocks = filteredBlocks.map((block) => ({
data: block, data: block,
id: block.id, id: block?.id ?? "",
sort_order: block.sort_order, sort_order: block?.sort_order ?? 0,
start_date: new Date(block.start_date ?? ""), start_date: new Date(block?.start_date ?? ""),
target_date: new Date(block.end_date ?? ""), target_date: new Date(block?.end_date ?? ""),
})) }));
: [];
return structuredBlocks;
};
const isAllowed = const isAllowed =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
@ -86,7 +90,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
<GanttChartRoot <GanttChartRoot
title="Cycles" title="Cycles"
loaderTitle="Cycles" loaderTitle="Cycles"
blocks={cycles ? blockFormat(cycles) : null} blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarToRender={(props) => <CycleGanttSidebar {...props} />} sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />} blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}

View File

@ -3,8 +3,8 @@ import { Dialog, Transition } from "@headlessui/react";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// hooks // hooks
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CycleForm } from "components/cycles"; import { CycleForm } from "components/cycles";
// types // types
@ -23,21 +23,21 @@ const cycleService = new CycleService();
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => { export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props; const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { postHogEventTracker },
} = useMobxStore();
// states // states
const [activeProject, setActiveProject] = useState<string>(projectId); const [activeProject, setActiveProject] = useState<string>(projectId);
// toast // store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { createCycle, updateCycleDetails } = useCycle();
// toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const createCycle = async (payload: Partial<ICycle>) => { const handleCreateCycle = async (payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore await createCycle(workspaceSlug, selectedProjectId, payload)
.createCycle(workspaceSlug, selectedProjectId, payload)
.then((res) => { .then((res) => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -61,11 +61,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}); });
}; };
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => { const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
.patchCycle(workspaceSlug, selectedProjectId, cycleId, payload)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
} }
if (isDateValid) { if (isDateValid) {
if (data) await updateCycle(data.id, payload); if (data) await handleUpdateCycle(data.id, payload);
else await createCycle(payload); else await handleCreateCycle(payload);
handleClose(); handleClose();
} else } else
setToastAlert({ setToastAlert({

View File

@ -3,11 +3,10 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react"; import { Disclosure, Popover, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// hooks // hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { SidebarProgressStats } from "components/core"; import { SidebarProgressStats } from "components/core";
@ -46,19 +45,21 @@ const cycleService = new CycleService();
// TODO: refactor the whole component // TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => { export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose } = props; const { cycleId, handleClose } = props;
// states
const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query; const { workspaceSlug, projectId, peekCycle } = router.query;
// store hooks
const { const {
cycle: cycleDetailsStore, eventTracker: { setTrackElement },
trackEvent: { setTrackElement }, } = useApplication();
user: { currentProjectRole }, const {
} = useMobxStore(); membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails } = useCycle();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; const cycleDetails = getCycleById(cycleId);
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -74,7 +75,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const submitChanges = (data: Partial<ICycle>) => { const submitChanges = (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
}; };
const handleCopyText = () => { const handleCopyText = () => {

View File

@ -2,8 +2,9 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { useCycle } from "hooks/store";
// components // components
import { import {
CycleAppliedFiltersRoot, CycleAppliedFiltersRoot,
@ -29,12 +30,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
projectId: string; projectId: string;
cycleId: string; cycleId: string;
}; };
// store hooks
const { const {
cycle: cycleStore, cycle: cycleStore,
cycleIssues: { loader, getIssues, fetchIssues }, cycleIssues: { loader, getIssues, fetchIssues },
cycleIssuesFilter: { issueFilters, fetchFilters }, cycleIssuesFilter: { issueFilters, fetchFilters },
} = useMobxStore(); } = useMobxStore();
const { getCycleById } = useCycle();
useSWR( useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null, workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
@ -48,7 +50,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined; const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
const cycleStatus = const cycleStatus =
cycleDetails?.start_date && cycleDetails?.end_date cycleDetails?.start_date && cycleDetails?.end_date
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)

View File

@ -1,6 +1,11 @@
import { GanttChartSquare, LayoutGrid, List } from "lucide-react"; import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
// types
import { TCycleLayout, TCycleView } from "types";
export const CYCLE_TAB_LIST = [ export const CYCLE_TAB_LIST: {
key: TCycleView;
name: string;
}[] = [
{ {
key: "all", key: "all",
name: "All", name: "All",
@ -23,7 +28,11 @@ export const CYCLE_TAB_LIST = [
}, },
]; ];
export const CYCLE_VIEW_LAYOUTS = [ export const CYCLE_VIEW_LAYOUTS: {
key: TCycleLayout;
icon: any;
title: string;
}[] = [
{ {
key: "list", key: "list",
icon: List, icon: List,

View File

@ -10,6 +10,7 @@ import { JoinProject } from "components/auth-screens";
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
// images // images
import emptyProject from "public/empty-state/project.svg"; import emptyProject from "public/empty-state/project.svg";
import { useApplication, useCycle, useModule, useProjectState, useUser } from "hooks/store";
interface IProjectAuthWrapper { interface IProjectAuthWrapper {
children: ReactNode; children: ReactNode;
@ -19,18 +20,22 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const { children } = props; const { children } = props;
// store // store
const { const {
user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
project: { fetchProjectDetails, workspaceProjects }, project: { fetchProjectDetails, workspaceProjects },
projectLabel: { fetchProjectLabels }, projectLabel: { fetchProjectLabels },
projectMember: { fetchProjectMembers }, projectMember: { fetchProjectMembers },
projectState: { fetchProjectStates },
projectEstimates: { fetchProjectEstimates }, projectEstimates: { fetchProjectEstimates },
cycle: { fetchCycles },
module: { fetchModules },
projectViews: { fetchAllViews }, projectViews: { fetchAllViews },
inbox: { fetchInboxesList, isInboxEnabled }, inbox: { fetchInboxesList, isInboxEnabled },
commandPalette: { toggleCreateProjectModal },
} = useMobxStore(); } = useMobxStore();
const {
commandPalette: { toggleCreateProjectModal },
} = useApplication();
const {
membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
} = useUser();
const { fetchAllCycles } = useCycle();
const { fetchModules } = useModule();
const { fetchProjectStates } = useProjectState();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -68,7 +73,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// fetching project cycles // fetching project cycles
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") : null workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null
); );
// fetching project modules // fetching project modules
useSWR( useSWR(
@ -80,7 +85,6 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null
); );
// TODO: fetching project pages
// fetching project inboxes if inbox is enabled // fetching project inboxes if inbox is enabled
useSWR( useSWR(
workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null,

View File

@ -1,9 +1,8 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useCycle } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
@ -19,24 +18,23 @@ import emptyCycle from "public/empty-state/cycle.svg";
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
const CycleDetailPage: NextPageWithLayout = () => { const CycleDetailPage: NextPageWithLayout = () => {
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query;
// store hooks
const { cycle: cycleStore } = useMobxStore(); const { fetchCycleDetails } = useCycle();
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const { error } = useSWR( const { error } = useSWR(
workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null, workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
workspaceSlug && projectId && cycleId workspaceSlug && projectId && cycleId
? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) ? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
: null : null
); );
const toggleSidebar = () => { const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
setValue(`${!isSidebarCollapsed}`);
};
return ( return (
<> <>

View File

@ -1,15 +1,17 @@
import { Fragment, useCallback, useEffect, useState, ReactElement } from "react"; import { Fragment, useCallback, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useCycle, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { CyclesHeader } from "components/headers"; import { CyclesHeader } from "components/headers";
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
import { NewEmptyState } from "components/common/new-empty-state";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// images // images
@ -20,64 +22,37 @@ import { NextPageWithLayout } from "types/app";
// constants // constants
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
// lib cookie
import { setLocalStorage, getLocalStorage } from "lib/local-storage";
import { NewEmptyState } from "components/common/new-empty-state";
// TODO: use-local-storage hook instead of lib file.
const ProjectCyclesPage: NextPageWithLayout = observer(() => { const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false); const [createModal, setCreateModal] = useState(false);
// store // store hooks
const { const {
cycle: cycleStore, membership: { currentProjectRole },
user: { currentProjectRole }, } = useUser();
} = useMobxStore(); const { projectAllCycles } = useCycle();
const { projectCycles } = cycleStore;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query; const { workspaceSlug, projectId, peekCycle } = router.query;
// local storage
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
const handleCurrentLayout = useCallback( const handleCurrentLayout = useCallback(
(_layout: TCycleLayout) => { (_layout: TCycleLayout) => {
if (projectId) { setCycleLayout(_layout);
setLocalStorage(`cycle_layout:${projectId}`, _layout);
cycleStore.setCycleLayout(_layout);
}
}, },
[cycleStore, projectId] [setCycleLayout]
); );
const handleCurrentView = useCallback( const handleCurrentView = useCallback(
(_view: TCycleView) => { (_view: TCycleView) => {
if (projectId) { setCycleTab(_view);
setLocalStorage(`cycle_view:${projectId}`, _view); if (_view === "draft") handleCurrentLayout("list");
cycleStore.setCycleView(_view);
if (_view === "draft" && cycleStore.cycleLayout === "gantt") {
handleCurrentLayout("list");
}
}
}, },
[cycleStore, projectId, handleCurrentLayout] [handleCurrentLayout, setCycleTab]
); );
useEffect(() => { const totalCycles = projectAllCycles?.length ?? 0;
if (projectId) {
const _viewKey = `cycle_view:${projectId}`;
const _viewValue = getLocalStorage(_viewKey);
if (_viewValue && _viewValue !== cycleStore?.cycleView) cycleStore.setCycleView(_viewValue as TCycleView);
else handleCurrentView("all");
const _layoutKey = `cycle_layout:${projectId}`;
const _layoutValue = getLocalStorage(_layoutKey);
if (_layoutValue && _layoutValue !== cycleStore?.cycleView)
cycleStore.setCycleLayout(_layoutValue as TCycleLayout);
else handleCurrentLayout("list");
}
}, [projectId, cycleStore, handleCurrentView, handleCurrentLayout]);
const cycleView = cycleStore?.cycleView;
const cycleLayout = cycleStore?.cycleLayout;
const totalCycles = projectCycles?.length ?? 0;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@ -117,11 +92,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tab.Group <Tab.Group
as="div" as="div"
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)} defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)} selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
onChange={(i) => { onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
handleCurrentView(CYCLE_TAB_LIST[i].key as TCycleView);
}}
> >
<div className="flex flex-col items-end justify-between gap-4 border-b border-custom-border-200 px-4 pb-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0"> <div className="flex flex-col items-end justify-between gap-4 border-b border-custom-border-200 px-4 pb-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll"> <Tab.List as="div" className="flex items-center overflow-x-scroll">
@ -138,26 +111,24 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab> </Tab>
))} ))}
</Tab.List> </Tab.List>
{cycleStore?.cycleView != "active" && ( {cycleTab !== "active" && (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1"> <div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => { {CYCLE_VIEW_LAYOUTS.map((layout) => {
if (layout.key === "gantt" && cycleStore?.cycleView === "draft") return null; if (layout.key === "gantt" && cycleTab === "draft") return null;
return ( return (
<Tooltip key={layout.key} tooltipContent={layout.title}> <Tooltip key={layout.key} tooltipContent={layout.title}>
<button <button
type="button" type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${ className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
cycleStore?.cycleLayout == layout.key cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
? "bg-custom-background-100 shadow-custom-shadow-2xs"
: ""
}`} }`}
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)} onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
> >
<layout.icon <layout.icon
strokeWidth={2} strokeWidth={2}
className={`h-3.5 w-3.5 ${ className={`h-3.5 w-3.5 ${
cycleStore?.cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200" cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`} }`}
/> />
</button> </button>
@ -170,10 +141,10 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && ( {cycleTab && cycleLayout && (
<CyclesView <CyclesView
filter={"all"} filter="all"
layout={cycleLayout as TCycleLayout} layout={cycleLayout}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
peekCycle={peekCycle?.toString()} peekCycle={peekCycle?.toString()}
@ -186,9 +157,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && ( {cycleTab && cycleLayout && (
<CyclesView <CyclesView
filter={"upcoming"} filter="upcoming"
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
@ -198,9 +169,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && workspaceSlug && projectId && ( {cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView <CyclesView
filter={"completed"} filter="completed"
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
@ -210,9 +181,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && workspaceSlug && projectId && ( {cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView <CyclesView
filter={"draft"} filter="draft"
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}

View File

@ -11,7 +11,7 @@ export class CycleService extends APIService {
super(API_BASE_URL); super(API_BASE_URL);
} }
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<any> { async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<ICycle> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -22,8 +22,8 @@ export class CycleService extends APIService {
async getCyclesWithParams( async getCyclesWithParams(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" cycleType?: "current"
): Promise<ICycle[]> { ): Promise<Record<string, ICycle>> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, {
params: { params: {
cycle_view: cycleType, cycle_view: cycleType,

View File

@ -1,7 +1,8 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
import set from "lodash/set"; import { set, omit } from "lodash";
import { isFuture, isPast } from "date-fns";
// types // types
import { ICycle, TCycleView, CycleDateCheckData } from "types"; import { ICycle, CycleDateCheckData } from "types";
// mobx // mobx
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
// services // services
@ -10,40 +11,30 @@ import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
export interface ICycleStore { export interface ICycleStore {
// states
loader: boolean; loader: boolean;
error: any | null; error: any | null;
// observables
cycleView: TCycleView;
cycleId: string | null;
cycleMap: { cycleMap: {
[projectId: string]: {
[cycleId: string]: ICycle; [cycleId: string]: ICycle;
}; };
activeCycleMap: {
[cycleId: string]: ICycle;
}; };
cycles: {
[projectId: string]: {
[filterType: string]: string[];
};
};
// computed // computed
getCycleById: (cycleId: string) => ICycle | null; projectAllCycles: string[] | null;
projectCycles: string[] | null;
projectCompletedCycles: string[] | null; projectCompletedCycles: string[] | null;
projectUpcomingCycles: string[] | null; projectUpcomingCycles: string[] | null;
projectDraftCycles: string[] | null; projectDraftCycles: string[] | null;
projectActiveCycle: string | null;
// computed actions
getCycleById: (cycleId: string) => ICycle | null;
getActiveCycleById: (cycleId: string) => ICycle | null;
// actions // actions
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>; validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise<Record<string, ICycle>>;
fetchCycles: ( fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise<Record<string, ICycle>>;
workspaceSlug: string,
projectId: string,
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
) => Promise<void>;
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>; createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
updateCycleDetails: ( updateCycleDetails: (
workspaceSlug: string, workspaceSlug: string,
@ -52,29 +43,19 @@ export interface ICycleStore {
data: Partial<ICycle> data: Partial<ICycle>
) => Promise<ICycle>; ) => Promise<ICycle>;
deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>; deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>; addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>; removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
} }
export class CycleStore implements ICycleStore { export class CycleStore implements ICycleStore {
// states
loader: boolean = false; loader: boolean = false;
error: any | null = null; error: any | null = null;
// observables
cycleView: TCycleView = "all";
cycleId: string | null = null;
cycleMap: { cycleMap: {
[projectId: string]: {
[cycleId: string]: ICycle; [cycleId: string]: ICycle;
};
} = {}; } = {};
cycles: { activeCycleMap: { [cycleId: string]: ICycle } = {};
[projectId: string]: {
[filterType: string]: string[];
};
} = {};
// root store // root store
rootStore; rootStore;
// services // services
@ -84,29 +65,28 @@ export class CycleStore implements ICycleStore {
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
loader: observable, // states
loader: observable.ref,
error: observable.ref, error: observable.ref,
// observables
cycleId: observable.ref,
cycleMap: observable, cycleMap: observable,
cycles: observable, activeCycleMap: observable,
// computed // computed
projectCycles: computed, projectAllCycles: computed,
projectCompletedCycles: computed, projectCompletedCycles: computed,
projectUpcomingCycles: computed, projectUpcomingCycles: computed,
projectDraftCycles: computed, projectDraftCycles: computed,
projectActiveCycle: computed,
// actions // computed actions
getCycleById: action, getCycleById: action,
getActiveCycleById: action,
fetchCycles: action, // actions
fetchAllCycles: action,
fetchActiveCycle: action,
fetchCycleDetails: action, fetchCycleDetails: action,
createCycle: action, createCycle: action,
updateCycleDetails: action, updateCycleDetails: action,
deleteCycle: action, deleteCycle: action,
addCycleToFavorites: action, addCycleToFavorites: action,
removeCycleFromFavorites: action, removeCycleFromFavorites: action,
}); });
@ -118,46 +98,86 @@ export class CycleStore implements ICycleStore {
} }
// computed // computed
get projectCycles() { get projectAllCycles() {
const projectId = this.rootStore.app.router.projectId; const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null; if (!projectId) return null;
return this.cycles[projectId]?.all || null;
const allCycles = Object.keys(this.cycleMap ?? {}).filter(
(cycleId) => this.cycleMap?.[cycleId]?.project === projectId
);
return allCycles || null;
} }
get projectCompletedCycles() { get projectCompletedCycles() {
const projectId = this.rootStore.app.router.projectId; const allCycles = this.projectAllCycles;
if (!projectId) return null; if (!allCycles) return null;
return this.cycles[projectId]?.completed || null; const completedCycles = allCycles.filter((cycleId) => {
const hasEndDatePassed = isPast(new Date(this.cycleMap?.[cycleId]?.end_date ?? ""));
return hasEndDatePassed;
});
return completedCycles || null;
} }
get projectUpcomingCycles() { get projectUpcomingCycles() {
const projectId = this.rootStore.app.router.projectId; const allCycles = this.projectAllCycles;
if (!projectId) return null; if (!allCycles) return null;
return this.cycles[projectId]?.upcoming || null; const upcomingCycles = allCycles.filter((cycleId) => {
const isStartDateUpcoming = isFuture(new Date(this.cycleMap?.[cycleId]?.start_date ?? ""));
return isStartDateUpcoming;
});
return upcomingCycles || null;
} }
get projectDraftCycles() { get projectDraftCycles() {
const projectId = this.rootStore.app.router.projectId; const allCycles = this.projectAllCycles;
if (!projectId) return null; if (!allCycles) return null;
return this.cycles[projectId]?.draft || null; const draftCycles = allCycles.filter((cycleId) => {
const cycleDetails = this.cycleMap?.[cycleId];
return !cycleDetails?.start_date && !cycleDetails?.end_date;
});
return draftCycles || null;
} }
getCycleById = (cycleId: string) => { get projectActiveCycle() {
const projectId = this.rootStore.app.router.projectId; const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null; if (!projectId) return null;
return this.cycleMap?.[projectId]?.[cycleId] || null; const activeCycle = Object.keys(this.activeCycleMap ?? {}).find(
}; (cycleId) => this.activeCycleMap?.[cycleId]?.project === projectId
);
return activeCycle || null;
}
/**
* @description returns cycle details by cycle id
* @param cycleId
* @returns
*/
getCycleById = (cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null;
/**
* @description returns active cycle details by cycle id
* @param cycleId
* @returns
*/
getActiveCycleById = (cycleId: string): ICycle | null => this.activeCycleMap?.[cycleId] ?? null;
// actions
validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => {
try { try {
const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
@ -168,27 +188,52 @@ export class CycleStore implements ICycleStore {
} }
}; };
fetchCycles = async ( fetchAllCycles = async (workspaceSlug: string, projectId: string) => {
workspaceSlug: string,
projectId: string,
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, projectId, params); const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId);
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId], cyclesResponse); Object.values(cyclesResponse).forEach((cycle) => {
set(this.cycles, [projectId, params], Object.keys(cyclesResponse)); set(this.cycleMap, [cycle.id], cycle);
});
this.loader = false; this.loader = false;
this.error = null; this.error = null;
}); });
return cyclesResponse;
} catch (error) { } catch (error) {
console.error("Failed to fetch project cycles in project store", error); console.error("Failed to fetch project cycles in project store", error);
this.loader = false; this.loader = false;
this.error = error; this.error = error;
throw error;
}
};
fetchActiveCycle = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, "current");
runInAction(() => {
Object.values(cyclesResponse).forEach((cycle) => {
set(this.activeCycleMap, [cycle.id], cycle);
});
this.loader = false;
this.error = null;
});
return cyclesResponse;
} catch (error) {
this.loader = false;
this.error = error;
throw error;
} }
}; };
@ -197,7 +242,8 @@ export class CycleStore implements ICycleStore {
const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId);
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, response?.id], response); set(this.cycleMap, [response.id], { ...this.cycleMap?.[response.id], ...response });
set(this.activeCycleMap, [response.id], { ...this.activeCycleMap?.[response.id], ...response });
}); });
return response; return response;
@ -212,12 +258,10 @@ export class CycleStore implements ICycleStore {
const response = await this.cycleService.createCycle(workspaceSlug, projectId, data); const response = await this.cycleService.createCycle(workspaceSlug, projectId, data);
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, response?.id], response); set(this.cycleMap, [response.id], response);
set(this.activeCycleMap, [response.id], response);
}); });
const _currentView = this.cycleView === "active" ? "current" : this.cycleView;
this.fetchCycles(workspaceSlug, projectId, _currentView);
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to create cycle from cycle store"); console.log("Failed to create cycle from cycle store");
@ -227,18 +271,14 @@ export class CycleStore implements ICycleStore {
updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>) => { updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>) => {
try { try {
const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data);
const currentCycle = this.cycleMap[projectId][cycleId];
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, cycleId], { ...currentCycle, ...data }); set(this.cycleMap, [cycleId], { ...this.cycleMap?.[cycleId], ...data });
set(this.activeCycleMap, [cycleId], { ...this.activeCycleMap?.[cycleId], ...data });
}); });
const _currentView = this.cycleView === "active" ? "current" : this.cycleView; return response;
this.fetchCycles(workspaceSlug, projectId, _currentView);
return _response;
} catch (error) { } catch (error) {
console.log("Failed to patch cycle from cycle store"); console.log("Failed to patch cycle from cycle store");
throw error; throw error;
@ -246,32 +286,36 @@ export class CycleStore implements ICycleStore {
}; };
deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => { deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try { const originalCycle = this.cycleMap[cycleId];
if (!this.cycleMap?.[projectId]?.[cycleId]) return; const originalActiveCycle = this.activeCycleMap[cycleId];
try {
runInAction(() => { runInAction(() => {
delete this.cycleMap[projectId][cycleId]; omit(this.cycleMap, [cycleId]);
omit(this.activeCycleMap, [cycleId]);
}); });
const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId);
return _response;
} catch (error) { } catch (error) {
console.log("Failed to delete cycle from cycle store"); console.log("Failed to delete cycle from cycle store");
const _currentView = this.cycleView === "active" ? "current" : this.cycleView; runInAction(() => {
this.fetchCycles(workspaceSlug, projectId, _currentView); set(this.cycleMap, [cycleId], originalCycle);
set(this.activeCycleMap, [cycleId], originalActiveCycle);
});
throw error; throw error;
} }
}; };
addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try { try {
const currentCycle = this.cycleMap[projectId][cycleId]; const currentCycle = this.getCycleById(cycleId);
if (currentCycle.is_favorite) return; const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], true); if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true);
}); });
// updating through api. // updating through api.
@ -279,10 +323,12 @@ export class CycleStore implements ICycleStore {
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to add cycle to favorites in the cycles store", error); const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], false); if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false);
}); });
throw error; throw error;
@ -291,22 +337,24 @@ export class CycleStore implements ICycleStore {
removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try { try {
const currentCycle = this.cycleMap[projectId][cycleId]; const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
if (!currentCycle.is_favorite) return;
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], false); if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false);
}); });
const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId);
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to remove cycle from favorites - Cycle Store", error); const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => { runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], true); if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true);
}); });
throw error; throw error;

View File

@ -1,5 +1,5 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
import set from "lodash/set"; import { set } from "lodash";
// services // services
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
import { ModuleService } from "services/module.service"; import { ModuleService } from "services/module.service";