diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 33254614c..59a0e0711 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -4,8 +4,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; -import { useApplication } from "hooks/store"; +import { useApplication, useCycle } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; @@ -71,20 +70,20 @@ export const ActiveCycleDetails: React.FC = observer((props const router = useRouter(); const { workspaceSlug, projectId } = props; // store hooks - const { cycle: cycleStore } = useMobxStore(); const { commandPalette: { toggleCreateCycleModal }, } = useApplication(); + const { fetchActiveCycle, projectActiveCycle, getActiveCycleById, addCycleToFavorites, removeCycleFromFavorites } = + useCycle(); // toast alert const { setToastAlert } = useToast(); const { isLoading } = useSWR( - workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, - workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null + workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, + workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null ); - const activeCycle = cycleStore.cycles?.[projectId]?.current || null; - const cycle = activeCycle ? activeCycle[0] : null; + const activeCycle = projectActiveCycle ? getActiveCycleById(projectActiveCycle) : null; const issues = (cycleStore?.active_cycle_issues as any) || null; // const { data: issues } = useSWR( @@ -97,14 +96,14 @@ export const ActiveCycleDetails: React.FC = observer((props // : null // ) as { data: IIssue[] | undefined }; - if (!cycle && isLoading) + if (!activeCycle && isLoading) return ( ); - if (!cycle) + if (!activeCycle) return (
@@ -129,24 +128,24 @@ export const ActiveCycleDetails: React.FC = observer((props
); - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); + const endDate = new Date(activeCycle.end_date ?? ""); + const startDate = new Date(activeCycle.start_date ?? ""); const groupedIssues: any = { - backlog: cycle.backlog_issues, - unstarted: cycle.unstarted_issues, - started: cycle.started_issues, - completed: cycle.completed_issues, - cancelled: cycle.cancelled_issues, + backlog: activeCycle.backlog_issues, + unstarted: activeCycle.unstarted_issues, + started: activeCycle.started_issues, + completed: activeCycle.completed_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) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -159,7 +158,7 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -171,7 +170,10 @@ export const ActiveCycleDetails: React.FC = observer((props const progressIndicatorData = stateGroups.map((group, index) => ({ id: index, name: group.title, - value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, + value: + activeCycle.total_issues > 0 + ? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100 + : 0, color: group.color, })); @@ -199,8 +201,8 @@ export const ActiveCycleDetails: React.FC = observer((props }`} /> - -

{truncateText(cycle.name, 70)}

+ +

{truncateText(activeCycle.name, 70)}

@@ -221,19 +223,19 @@ export const ActiveCycleDetails: React.FC = observer((props {cycleStatus === "current" ? ( - {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left ) : cycleStatus === "upcoming" ? ( - {findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left ) : cycleStatus === "completed" ? ( - {cycle.total_issues - cycle.completed_issues > 0 && ( + {activeCycle.total_issues - activeCycle.completed_issues > 0 && ( @@ -247,7 +249,7 @@ export const ActiveCycleDetails: React.FC = observer((props cycleStatus )} - {cycle.is_favorite ? ( + {activeCycle.is_favorite ? (
- +
@@ -469,15 +471,18 @@ export const ActiveCycleDetails: React.FC = observer((props - Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)} + + Pending Issues -{" "} + {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)} +
diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index d6806eaf0..b7acff358 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,10 +1,8 @@ import React, { useEffect } from "react"; - import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCycle } from "hooks/store"; // components import { CycleDetailsSidebar } from "./sidebar"; @@ -14,14 +12,13 @@ type Props = { }; export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug }) => { + // router const router = useRouter(); const { peekCycle } = router.query; - + // refs const ref = React.useRef(null); - - const { cycle: cycleStore } = useMobxStore(); - - const { fetchCycleWithId } = cycleStore; + // store hooks + const { fetchCycleDetails } = useCycle(); const handleClose = () => { delete router.query.peekCycle; @@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa useEffect(() => { if (!peekCycle) return; - fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString()); - }, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]); + fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString()); + }, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]); return ( <> diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index f020b0998..1349b763b 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; // hooks +import { useApplication, useCycle, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; @@ -17,10 +18,6 @@ import { renderShortMonthDate, } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; -// types -import { ICycle } from "types"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // constants import { CYCLE_STATUS } from "constants/cycle"; import { EUserWorkspaceRoles } from "constants/workspace"; @@ -28,61 +25,33 @@ import { EUserWorkspaceRoles } from "constants/workspace"; export interface ICyclesBoardCard { workspaceSlug: string; projectId: string; - cycle: ICycle; + cycleId: string; } export const CyclesBoardCard: FC = (props) => { - const { cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { setTrackElement }, - user: userStore, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); + const { cycleId, workspaceSlug, projectId } = props; // states const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - // computed - 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; - + // router const router = useRouter(); - - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); - - const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); - - const cycleTotalIssues = - cycle.backlog_issues + - cycle.unstarted_issues + - cycle.started_issues + - cycle.completed_issues + - 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"; + // store + const { + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); 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({ type: "success", title: "Link Copied!", @@ -95,7 +64,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -108,7 +77,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -137,14 +106,48 @@ export const CyclesBoardCard: FC = (props) => { router.push({ 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 (
setUpdateModal(false)} workspaceSlug={workspaceSlug} @@ -152,22 +155,22 @@ export const CyclesBoardCard: FC = (props) => { /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
- - {cycle.name} + + {cycleDetails.name}
@@ -180,7 +183,7 @@ export const CyclesBoardCard: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -196,11 +199,11 @@ export const CyclesBoardCard: FC = (props) => { {issueCount}
- {cycle.assignees.length > 0 && ( - + {cycleDetails.assignees.length > 0 && ( +
- {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -241,7 +244,7 @@ export const CyclesBoardCard: FC = (props) => { )}
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index e69089664..967e8a395 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -4,11 +4,9 @@ import { observer } from "mobx-react-lite"; import { useApplication } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -// types -import { ICycle } from "types"; export interface ICyclesBoard { - cycles: ICycle[]; + cycleIds: string[]; filter: string; workspaceSlug: string; projectId: string; @@ -16,13 +14,13 @@ export interface ICyclesBoard { } export const CyclesBoard: FC = observer((props) => { - const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; + const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; // store hooks const { commandPalette: commandPaletteStore } = useApplication(); return ( <> - {cycles.length > 0 ? ( + {cycleIds?.length > 0 ? (
= observer((props) => { : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" } auto-rows-max transition-all `} > - {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
void; handleDeleteCycle?: () => void; handleAddToFavorites?: () => void; @@ -37,52 +33,29 @@ type TCyclesListItem = { }; export const CyclesListItem: FC = (props) => { - const { cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { setTrackElement }, - user: userStore, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); + const { cycleId, workspaceSlug, projectId } = props; // states const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - // computed - 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; - + // router const router = useRouter(); - - const cycleTotalIssues = - cycle.backlog_issues + - cycle.unstarted_issues + - cycle.started_issues + - cycle.completed_issues + - cycle.cancelled_issues; - - const renderDate = cycle.start_date || cycle.end_date; - - 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); + // store hooks + const { + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); 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({ type: "success", title: "Link Copied!", @@ -95,7 +68,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -108,7 +81,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -137,27 +110,56 @@ export const CyclesListItem: FC = (props) => { router.push({ 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 ( <> setUpdateModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
@@ -181,8 +183,8 @@ export const CyclesListItem: FC = (props) => { - - {cycle.name} + + {cycleDetails.name}
@@ -202,7 +204,7 @@ export const CyclesListItem: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -216,11 +218,11 @@ export const CyclesListItem: FC = (props) => { )} - +
- {cycle.assignees.length > 0 ? ( + {cycleDetails.assignees.length > 0 ? ( - {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -232,7 +234,7 @@ export const CyclesListItem: FC = (props) => {
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 05fa9b92f..686937b71 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -6,18 +6,16 @@ import { useApplication } from "hooks/store"; import { CyclePeekOverview, CyclesListItem } from "components/cycles"; // ui import { Loader } from "@plane/ui"; -// types -import { ICycle } from "types"; export interface ICyclesList { - cycles: ICycle[]; + cycleIds: string[]; filter: string; workspaceSlug: string; projectId: string; } export const CyclesList: FC = observer((props) => { - const { cycles, filter, workspaceSlug, projectId } = props; + const { cycleIds, filter, workspaceSlug, projectId } = props; // store hooks const { commandPalette: commandPaletteStore, @@ -26,14 +24,14 @@ export const CyclesList: FC = observer((props) => { return ( <> - {cycles ? ( + {cycleIds ? ( <> - {cycles.length > 0 ? ( + {cycleIds.length > 0 ? (
- {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
= observer((props) => { const { filter, layout, workspaceSlug, projectId, peekCycle } = props; - - // store - 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 - ); + // store hooks + const { projectCompletedCycles, projectDraftCycles, projectUpcomingCycles, projectAllCycles } = useCycle(); const cyclesList = filter === "completed" - ? cycleStore.projectCompletedCycles + ? projectCompletedCycles : filter === "draft" - ? cycleStore.projectDraftCycles - : filter === "upcoming" - ? cycleStore.projectUpcomingCycles - : cycleStore.projectCycles; + ? projectDraftCycles + : filter === "upcoming" + ? projectUpcomingCycles + : projectAllCycles; return ( <> {layout === "list" && ( <> {cyclesList ? ( - + ) : ( @@ -59,7 +51,7 @@ export const CyclesView: FC = observer((props) => { <> {cyclesList ? ( = observer((props) => { {layout === "gantt" && ( <> {cyclesList ? ( - + ) : ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 33c6254df..dd4eda4a9 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,17 +1,15 @@ import { Fragment, useState } from "react"; -// next import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; +// hooks +import { useApplication, useCycle } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; // types import { ICycle } from "types"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; interface ICycleDelete { cycle: ICycle; @@ -23,56 +21,51 @@ interface ICycleDelete { export const CycleDeleteModal: React.FC = observer((props) => { const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); // states const [loader, setLoader] = useState(false); + // router const router = useRouter(); const { cycleId, peekCycle } = router.query; + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { deleteCycle } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const formSubmit = async () => { + if (!cycle) return; + setLoader(true); - if (cycle?.id) - try { - await cycleStore - .removeCycle(workspaceSlug, projectId, cycle?.id) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle deleted successfully.", - }); - postHogEventTracker("CYCLE_DELETE", { - state: "SUCCESS", - }); - }) - .catch(() => { - postHogEventTracker("CYCLE_DELETE", { - state: "FAILED", - }); + try { + await deleteCycle(workspaceSlug, projectId, cycle.id) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Cycle deleted successfully.", + }); + postHogEventTracker("CYCLE_DELETE", { + state: "SUCCESS", + }); + }) + .catch(() => { + postHogEventTracker("CYCLE_DELETE", { + state: "FAILED", }); - - if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); - - handleClose(); - } catch (error) { - setToastAlert({ - type: "error", - title: "Warning!", - message: "Something went wrong please try again later.", }); - } - else + + if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); + + handleClose(); + } catch (error) { setToastAlert({ type: "error", title: "Warning!", message: "Something went wrong please try again later.", }); + } setLoader(false); }; diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 4085b4e34..17338467c 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { KeyedMutator } from "swr"; // hooks -import { useUser } from "hooks/store"; +import { useCycle, useUser } from "hooks/store"; // services import { CycleService } from "services/cycle.service"; // components @@ -16,7 +16,7 @@ import { EUserWorkspaceRoles } from "constants/workspace"; type Props = { workspaceSlug: string; - cycles: ICycle[]; + cycleIds: string[]; mutateCycles?: KeyedMutator; }; @@ -24,7 +24,7 @@ type Props = { const cycleService = new CycleService(); export const CyclesListGanttChartView: FC = observer((props) => { - const { cycles, mutateCycles } = props; + const { cycleIds, mutateCycles } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -32,6 +32,7 @@ export const CyclesListGanttChartView: FC = observer((props) => { const { membership: { currentProjectRole }, } = useUser(); + const { getCycleById } = useCycle(); const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { if (!workspaceSlug) return; @@ -65,18 +66,21 @@ export const CyclesListGanttChartView: FC = observer((props) => { cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload); }; - const blockFormat = (blocks: ICycle[]) => - blocks && blocks.length > 0 - ? blocks - .filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)) - .map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.end_date ?? ""), - })) - : []; + const blockFormat = (blocks: (ICycle | null)[]) => { + if (!blocks) return []; + + const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date); + + const structuredBlocks = filteredBlocks.map((block) => ({ + data: block, + id: block?.id ?? "", + sort_order: block?.sort_order ?? 0, + start_date: new Date(block?.start_date ?? ""), + target_date: new Date(block?.end_date ?? ""), + })); + + return structuredBlocks; + }; const isAllowed = currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole); @@ -86,7 +90,7 @@ export const CyclesListGanttChartView: FC = observer((props) => { getCycleById(c))) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } blockToRender={(data: ICycle) => } diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 665f9865b..5aa0b0ce4 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -3,8 +3,8 @@ import { Dialog, Transition } from "@headlessui/react"; // services import { CycleService } from "services/cycle.service"; // hooks +import { useApplication, useCycle } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CycleForm } from "components/cycles"; // types @@ -23,21 +23,21 @@ const cycleService = new CycleService(); export const CycleCreateUpdateModal: React.FC = (props) => { const { isOpen, handleClose, data, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); // states const [activeProject, setActiveProject] = useState(projectId); - // toast + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { createCycle, updateCycleDetails } = useCycle(); + // toast alert const { setToastAlert } = useToast(); - const createCycle = async (payload: Partial) => { + const handleCreateCycle = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; + const selectedProjectId = payload.project ?? projectId.toString(); - await cycleStore - .createCycle(workspaceSlug, selectedProjectId, payload) + await createCycle(workspaceSlug, selectedProjectId, payload) .then((res) => { setToastAlert({ type: "success", @@ -61,11 +61,11 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }; - const updateCycle = async (cycleId: string, payload: Partial) => { + const handleUpdateCycle = async (cycleId: string, payload: Partial) => { if (!workspaceSlug || !projectId) return; + const selectedProjectId = payload.project ?? projectId.toString(); - await cycleStore - .patchCycle(workspaceSlug, selectedProjectId, cycleId, payload) + await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload) .then(() => { setToastAlert({ type: "success", @@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } if (isDateValid) { - if (data) await updateCycle(data.id, payload); - else await createCycle(payload); + if (data) await handleUpdateCycle(data.id, payload); + else await handleCreateCycle(payload); handleClose(); } else setToastAlert({ diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index fa7fef008..fb1a4bf47 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -3,11 +3,10 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useForm } from "react-hook-form"; import { Disclosure, Popover, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { CycleService } from "services/cycle.service"; // hooks +import { useApplication, useCycle, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; @@ -46,19 +45,21 @@ const cycleService = new CycleService(); // TODO: refactor the whole component export const CycleDetailsSidebar: React.FC = observer((props) => { const { cycleId, handleClose } = props; - + // states const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; - + // store hooks const { - cycle: cycleDetailsStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById, updateCycleDetails } = useCycle(); - const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; + const cycleDetails = getCycleById(cycleId); const { setToastAlert } = useToast(); @@ -74,7 +75,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !cycleId) return; - cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); + updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); }; const handleCopyText = () => { diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 0d40a7f06..b20aa36b9 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -2,8 +2,9 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; -// mobx store +// hooks import { useMobxStore } from "lib/mobx/store-provider"; +import { useCycle } from "hooks/store"; // components import { CycleAppliedFiltersRoot, @@ -29,12 +30,13 @@ export const CycleLayoutRoot: React.FC = observer(() => { projectId: string; cycleId: string; }; - + // store hooks const { cycle: cycleStore, cycleIssues: { loader, getIssues, fetchIssues }, cycleIssuesFilter: { issueFilters, fetchFilters }, } = useMobxStore(); + const { getCycleById } = useCycle(); useSWR( 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 cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined; + const cycleDetails = cycleId ? getCycleById(cycleId) : undefined; const cycleStatus = cycleDetails?.start_date && cycleDetails?.end_date ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 697dc3660..9e1e1b39e 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -1,6 +1,11 @@ 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", 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", icon: List, diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index 5f162b302..0a3b1e39b 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -10,6 +10,7 @@ import { JoinProject } from "components/auth-screens"; import { EmptyState } from "components/common"; // images import emptyProject from "public/empty-state/project.svg"; +import { useApplication, useCycle, useModule, useProjectState, useUser } from "hooks/store"; interface IProjectAuthWrapper { children: ReactNode; @@ -19,18 +20,22 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children } = props; // store const { - user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, project: { fetchProjectDetails, workspaceProjects }, projectLabel: { fetchProjectLabels }, projectMember: { fetchProjectMembers }, - projectState: { fetchProjectStates }, projectEstimates: { fetchProjectEstimates }, - cycle: { fetchCycles }, - module: { fetchModules }, projectViews: { fetchAllViews }, inbox: { fetchInboxesList, isInboxEnabled }, - commandPalette: { toggleCreateProjectModal }, } = useMobxStore(); + const { + commandPalette: { toggleCreateProjectModal }, + } = useApplication(); + const { + membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, + } = useUser(); + const { fetchAllCycles } = useCycle(); + const { fetchModules } = useModule(); + const { fetchProjectStates } = useProjectState(); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -68,7 +73,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { // fetching project cycles useSWR( 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 useSWR( @@ -80,7 +85,6 @@ export const ProjectAuthWrapper: FC = observer((props) => { workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null ); - // TODO: fetching project pages // fetching project inboxes if inbox is enabled useSWR( workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index e54c59ba7..dd1dc3fdc 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,9 +1,8 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useCycle } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -19,24 +18,23 @@ import emptyCycle from "public/empty-state/cycle.svg"; import { NextPageWithLayout } from "types/app"; const CycleDetailPage: NextPageWithLayout = () => { + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - - const { cycle: cycleStore } = useMobxStore(); + // store hooks + const { fetchCycleDetails } = useCycle(); const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const { error } = useSWR( - workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null, + workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null, workspaceSlug && projectId && cycleId - ? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) + ? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) : null ); - const toggleSidebar = () => { - setValue(`${!isSidebarCollapsed}`); - }; + const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`); return ( <> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index c987408b0..395d3ae56 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -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 { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; import { Plus } from "lucide-react"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useCycle, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { NewEmptyState } from "components/common/new-empty-state"; // ui import { Tooltip } from "@plane/ui"; // images @@ -20,64 +22,37 @@ import { NextPageWithLayout } from "types/app"; // constants import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; 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 [createModal, setCreateModal] = useState(false); - // store + // store hooks const { - cycle: cycleStore, - user: { currentProjectRole }, - } = useMobxStore(); - const { projectCycles } = cycleStore; + membership: { currentProjectRole }, + } = useUser(); + const { projectAllCycles } = useCycle(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; + // local storage + const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); + const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); const handleCurrentLayout = useCallback( (_layout: TCycleLayout) => { - if (projectId) { - setLocalStorage(`cycle_layout:${projectId}`, _layout); - cycleStore.setCycleLayout(_layout); - } + setCycleLayout(_layout); }, - [cycleStore, projectId] + [setCycleLayout] ); const handleCurrentView = useCallback( (_view: TCycleView) => { - if (projectId) { - setLocalStorage(`cycle_view:${projectId}`, _view); - cycleStore.setCycleView(_view); - if (_view === "draft" && cycleStore.cycleLayout === "gantt") { - handleCurrentLayout("list"); - } - } + setCycleTab(_view); + if (_view === "draft") handleCurrentLayout("list"); }, - [cycleStore, projectId, handleCurrentLayout] + [handleCurrentLayout, setCycleTab] ); - useEffect(() => { - 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 totalCycles = projectAllCycles?.length ?? 0; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; @@ -117,11 +92,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { i.key == cycleStore?.cycleView)} - selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)} - onChange={(i) => { - handleCurrentView(CYCLE_TAB_LIST[i].key as TCycleView); - }} + defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} + selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} + onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")} >
@@ -138,26 +111,24 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { ))} - {cycleStore?.cycleView != "active" && ( + {cycleTab !== "active" && (
{CYCLE_VIEW_LAYOUTS.map((layout) => { - if (layout.key === "gantt" && cycleStore?.cycleView === "draft") return null; + if (layout.key === "gantt" && cycleTab === "draft") return null; return ( @@ -170,10 +141,10 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { - {cycleView && cycleLayout && ( + {cycleTab && cycleLayout && ( { - {cycleView && cycleLayout && ( + {cycleTab && cycleLayout && ( { - {cycleView && cycleLayout && workspaceSlug && projectId && ( + {cycleTab && cycleLayout && workspaceSlug && projectId && ( { - {cycleView && cycleLayout && workspaceSlug && projectId && ( + {cycleTab && cycleLayout && workspaceSlug && projectId && ( { + async createCycle(workspaceSlug: string, projectId: string, data: any): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) .then((response) => response?.data) .catch((error) => { @@ -22,8 +22,8 @@ export class CycleService extends APIService { async getCyclesWithParams( workspaceSlug: string, projectId: string, - cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" - ): Promise { + cycleType?: "current" + ): Promise> { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { params: { cycle_view: cycleType, diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index 695789998..03f11f48a 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -1,7 +1,8 @@ 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 -import { ICycle, TCycleView, CycleDateCheckData } from "types"; +import { ICycle, CycleDateCheckData } from "types"; // mobx import { RootStore } from "store/root.store"; // services @@ -10,40 +11,30 @@ import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; export interface ICycleStore { + // states loader: boolean; error: any | null; - - cycleView: TCycleView; - - cycleId: string | null; + // observables cycleMap: { - [projectId: string]: { - [cycleId: string]: ICycle; - }; + [cycleId: string]: ICycle; }; - cycles: { - [projectId: string]: { - [filterType: string]: string[]; - }; + activeCycleMap: { + [cycleId: string]: ICycle; }; - // computed - getCycleById: (cycleId: string) => ICycle | null; - projectCycles: string[] | null; + projectAllCycles: string[] | null; projectCompletedCycles: string[] | null; projectUpcomingCycles: string[] | null; projectDraftCycles: string[] | null; - + projectActiveCycle: string | null; + // computed actions + getCycleById: (cycleId: string) => ICycle | null; + getActiveCycleById: (cycleId: string) => ICycle | null; // actions validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; - - fetchCycles: ( - workspaceSlug: string, - projectId: string, - params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" - ) => Promise; + fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise>; + fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise>; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateCycleDetails: ( workspaceSlug: string, @@ -52,29 +43,19 @@ export interface ICycleStore { data: Partial ) => Promise; deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; } export class CycleStore implements ICycleStore { + // states loader: boolean = false; error: any | null = null; - - cycleView: TCycleView = "all"; - - cycleId: string | null = null; + // observables cycleMap: { - [projectId: string]: { - [cycleId: string]: ICycle; - }; + [cycleId: string]: ICycle; } = {}; - cycles: { - [projectId: string]: { - [filterType: string]: string[]; - }; - } = {}; - + activeCycleMap: { [cycleId: string]: ICycle } = {}; // root store rootStore; // services @@ -84,29 +65,28 @@ export class CycleStore implements ICycleStore { constructor(_rootStore: RootStore) { makeObservable(this, { - loader: observable, + // states + loader: observable.ref, error: observable.ref, - - cycleId: observable.ref, + // observables cycleMap: observable, - cycles: observable, - + activeCycleMap: observable, // computed - projectCycles: computed, + projectAllCycles: computed, projectCompletedCycles: computed, projectUpcomingCycles: computed, projectDraftCycles: computed, - - // actions + projectActiveCycle: computed, + // computed actions getCycleById: action, - - fetchCycles: action, + getActiveCycleById: action, + // actions + fetchAllCycles: action, + fetchActiveCycle: action, fetchCycleDetails: action, - createCycle: action, updateCycleDetails: action, deleteCycle: action, - addCycleToFavorites: action, removeCycleFromFavorites: action, }); @@ -118,46 +98,86 @@ export class CycleStore implements ICycleStore { } // computed - get projectCycles() { + get projectAllCycles() { const projectId = this.rootStore.app.router.projectId; 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() { - 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() { - 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() { - 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; 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) => { try { const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); @@ -168,27 +188,52 @@ export class CycleStore implements ICycleStore { } }; - fetchCycles = async ( - workspaceSlug: string, - projectId: string, - params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" - ) => { + fetchAllCycles = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; this.error = null; - const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params); + const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId); runInAction(() => { - set(this.cycleMap, [projectId], cyclesResponse); - set(this.cycles, [projectId, params], Object.keys(cyclesResponse)); + Object.values(cyclesResponse).forEach((cycle) => { + set(this.cycleMap, [cycle.id], cycle); + }); this.loader = false; this.error = null; }); + + return cyclesResponse; } catch (error) { console.error("Failed to fetch project cycles in project store", error); this.loader = false; 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); 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; @@ -212,12 +258,10 @@ export class CycleStore implements ICycleStore { const response = await this.cycleService.createCycle(workspaceSlug, projectId, data); 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; } catch (error) { 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) => { try { - const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); - - const currentCycle = this.cycleMap[projectId][cycleId]; + const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); 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; - this.fetchCycles(workspaceSlug, projectId, _currentView); - - return _response; + return response; } catch (error) { console.log("Failed to patch cycle from cycle store"); throw error; @@ -246,32 +286,36 @@ export class CycleStore implements ICycleStore { }; deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => { - try { - if (!this.cycleMap?.[projectId]?.[cycleId]) return; + const originalCycle = this.cycleMap[cycleId]; + const originalActiveCycle = this.activeCycleMap[cycleId]; + try { runInAction(() => { - delete this.cycleMap[projectId][cycleId]; + omit(this.cycleMap, [cycleId]); + omit(this.activeCycleMap, [cycleId]); }); - const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); - - return _response; + await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); } catch (error) { console.log("Failed to delete cycle from cycle store"); - const _currentView = this.cycleView === "active" ? "current" : this.cycleView; - this.fetchCycles(workspaceSlug, projectId, _currentView); + runInAction(() => { + set(this.cycleMap, [cycleId], originalCycle); + set(this.activeCycleMap, [cycleId], originalActiveCycle); + }); + throw error; } }; addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { try { - const currentCycle = this.cycleMap[projectId][cycleId]; - if (currentCycle.is_favorite) return; + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); 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. @@ -279,10 +323,12 @@ export class CycleStore implements ICycleStore { return response; } 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(() => { - 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; @@ -291,22 +337,24 @@ export class CycleStore implements ICycleStore { removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { try { - const currentCycle = this.cycleMap[projectId][cycleId]; - - if (!currentCycle.is_favorite) return; + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); 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); return response; } catch (error) { - console.log("Failed to remove cycle from favorites - Cycle Store", error); + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); 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; diff --git a/web/store/module.store.ts b/web/store/module.store.ts index a0f999d27..8b636aed0 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -1,5 +1,5 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import set from "lodash/set"; +import { set } from "lodash"; // services import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service";