diff --git a/web/components/common/empty-state.tsx b/web/components/common/empty-state.tsx index e39b10801f2..6cccf614e48 100644 --- a/web/components/common/empty-state.tsx +++ b/web/components/common/empty-state.tsx @@ -28,22 +28,14 @@ export const EmptyState: React.FC = ({ isFullScreen = true, disabled = false, }) => ( -
+
{primaryButton?.text}
{title}
{description &&

{description}

}
{primaryButton && ( - + {primaryButton.icon} {primaryButton.text} diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 09ee9853d62..eb1fb8a0922 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,14 +1,12 @@ -import React from "react"; - +import { MouseEvent } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; - import useSWR, { mutate } from "swr"; - // services import { CycleService } from "services/cycle.service"; // hooks import useToast from "hooks/use-toast"; +import { useMobxStore } from "lib/mobx/store-provider"; // ui import { AssigneesList } from "components/ui/avatar"; import { SingleProgressStats } from "components/core"; @@ -16,7 +14,7 @@ import { Loader, Tooltip, LinearProgressIndicator } from "@plane/ui"; // components import ProgressChart from "components/core/sidebar/progress-chart"; import { ActiveCycleProgressStats } from "components/cycles"; - +import { ViewIssueLabel } from "components/issues"; // icons import { CalendarDaysIcon } from "@heroicons/react/20/solid"; import { PriorityIcon } from "components/icons/priority-icon"; @@ -31,8 +29,6 @@ import { StateGroupIcon, } from "components/icons"; import { StarIcon } from "@heroicons/react/24/outline"; -// components -import { ViewIssueLabel } from "components/issues"; // helpers import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; @@ -69,34 +65,43 @@ const stateGroups = [ }, ]; -// services -const cycleService = new CycleService(); +interface IActiveCycleDetails { + workspaceSlug: string; + projectId: string; +} + +export const ActiveCycleDetails: React.FC = (props) => { + // services + const cycleService = new CycleService(); -export const ActiveCycleDetails: React.FC = () => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + + const { workspaceSlug, projectId } = props; + + const { cycle: cycleStore } = useMobxStore(); const { setToastAlert } = useToast(); - const { data: currentCycle } = useSWR( - workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current") - : null + const { isLoading } = useSWR( + workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, + workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null ); - const cycle = currentCycle ? currentCycle[0] : null; - const { data: issues } = useSWR( - workspaceSlug && projectId && cycle?.id ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" }) : null, - workspaceSlug && projectId && cycle?.id - ? () => - cycleService.getCycleIssuesWithParams(workspaceSlug as string, projectId as string, cycle.id, { - priority: "urgent,high", - }) - : null - ) as { data: IIssue[] | undefined }; + const activeCycle = cycleStore.cycles?.[projectId] || null; + const cycle = activeCycle ? activeCycle[0] : null; + const issues = (cycleStore?.active_cycle_issues as any) || null; + + // const { data: issues } = useSWR( + // workspaceSlug && projectId && cycle?.id ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" }) : null, + // workspaceSlug && projectId && cycle?.id + // ? () => + // cycleService.getCycleIssuesWithParams(workspaceSlug as string, projectId as string, cycle.id, { + // priority: "urgent,high", + // }) + // : null + // ) as { data: IIssue[] | undefined }; - if (!currentCycle) + if (isLoading) return ( @@ -146,70 +151,28 @@ export const ActiveCycleDetails: React.FC = () => { const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; + const handleAddToFavorites = (e: MouseEvent) => { + e.preventDefault(); + if (!workspaceSlug || !projectId) return; - mutate( - CURRENT_CYCLE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - mutate( - CYCLES_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - cycleService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", }); + }); }; - const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - mutate( - CURRENT_CYCLE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - mutate( - CYCLES_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); + const handleRemoveFromFavorites = (e: MouseEvent) => { + e.preventDefault(); + if (!workspaceSlug || !projectId) return; - cycleService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => { + cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", + message: "Couldn't add the cycle to favorites. Please try again.", }); }); }; @@ -296,8 +259,7 @@ export const ActiveCycleDetails: React.FC = () => { {cycle.is_favorite ? ( - ) : ( - - )} -
- - {!isCompleted && ( - - - - Edit Cycle - - - )} - {!isCompleted && ( - - - - Delete cycle - - - )} - - - - Copy cycle link - - - -
-
+ > + {cycleStatus === "current" ? ( + + {cycle.total_issues > 0 ? ( + <> + + {Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} % + + ) : ( + No issues present + )} + + ) : cycleStatus === "upcoming" ? ( + + Yet to start + + ) : cycleStatus === "completed" ? ( + + + {100} % + + ) : ( + + + {cycleStatus} + + )} + + + + {/* cycle favorite */} + {cycle.is_favorite ? ( + + ) : ( + + )}
-
- - + + +
+ +
+ + {!isCompleted && ( + setUpdateModal(true)}> + + + Edit Cycle + + + )} + + {!isCompleted && ( + setDeleteModal(true)}> + + + Delete cycle + + + )} + + + + + Copy cycle link + + + +
- + + setUpdateModal(false)} + onSubmit={updateModalCallback} + workspaceSlug={workspaceSlug} + projectId={projectId} + /> + + setDeleteModal(false)} + onSubmit={deleteModalCallback} + workspaceSlug={workspaceSlug} + projectId={projectId} + /> + ); }; diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index ec8a40837ec..947bd1fea3f 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,17 +1,20 @@ import { FC } from "react"; +// components +import { CyclesListItem } from "./cycles-list-item"; // ui import { Loader } from "@plane/ui"; // types import { ICycle } from "types"; -import { CyclesListItem } from "./cycles-list-item"; export interface ICyclesList { cycles: ICycle[]; filter: string; + workspaceSlug: string; + projectId: string; } export const CyclesList: FC = (props) => { - const { cycles, filter } = props; + const { cycles, filter, workspaceSlug, projectId } = props; return (
@@ -22,7 +25,7 @@ export const CyclesList: FC = (props) => { {cycles.map((cycle) => (
- +
))} diff --git a/web/components/cycles/cycles-view-legacy.tsx b/web/components/cycles/cycles-view-legacy.tsx deleted file mode 100644 index 55c67524b7e..00000000000 --- a/web/components/cycles/cycles-view-legacy.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; -import { KeyedMutator, mutate } from "swr"; -// services -import { CycleService } from "services/cycle.service"; -// hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; -import useLocalStorage from "hooks/use-local-storage"; -// components -import { - CreateUpdateCycleModal, - CyclesListGanttChartView, - DeleteCycleModal, - SingleCycleCard, - SingleCycleList, -} from "components/cycles"; -// ui -import { Loader } from "@plane/ui"; -// helpers -import { getDateRangeStatus } from "helpers/date-time.helper"; -// types -import { ICycle } from "types"; -// fetch-keys -import { - COMPLETED_CYCLES_LIST, - CURRENT_CYCLE_LIST, - CYCLES_LIST, - DRAFT_CYCLES_LIST, - UPCOMING_CYCLES_LIST, -} from "constants/fetch-keys"; - -type Props = { - cycles: ICycle[] | undefined; - mutateCycles?: KeyedMutator; - viewType: string | null; -}; - -const cycleService = new CycleService(); - -export const CyclesView: React.FC = ({ cycles, mutateCycles, viewType }) => { - const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); - const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState(null); - - const [deleteCycleModal, setDeleteCycleModal] = useState(false); - const [selectedCycleToDelete, setSelectedCycleToDelete] = useState(null); - - const { storedValue: cycleTab } = useLocalStorage("cycle_tab", "all"); - console.log("cycleTab", cycleTab); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { user } = useUserAuth(); - const { setToastAlert } = useToast(); - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycleToUpdate(cycle); - setCreateUpdateCycleModal(true); - }; - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleToDelete(cycle); - setDeleteCycleModal(true); - }; - - const handleAddToFavorites = (cycle: ICycle) => { - if (!workspaceSlug || !projectId) return; - - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); - - const fetchKey = - cycleStatus === "current" - ? CURRENT_CYCLE_LIST(projectId as string) - : cycleStatus === "upcoming" - ? UPCOMING_CYCLES_LIST(projectId as string) - : cycleStatus === "completed" - ? COMPLETED_CYCLES_LIST(projectId as string) - : DRAFT_CYCLES_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - mutate( - CYCLES_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - cycleService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = (cycle: ICycle) => { - if (!workspaceSlug || !projectId) return; - - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); - - const fetchKey = - cycleStatus === "current" - ? CURRENT_CYCLE_LIST(projectId as string) - : cycleStatus === "upcoming" - ? UPCOMING_CYCLES_LIST(projectId as string) - : cycleStatus === "completed" - ? COMPLETED_CYCLES_LIST(projectId as string) - : DRAFT_CYCLES_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - mutate( - CYCLES_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - cycleService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", - }); - }); - }; - - return ( - <> - setCreateUpdateCycleModal(false)} - data={selectedCycleToUpdate} - user={user} - /> - - {cycles ? ( - cycles.length > 0 ? ( - viewType === "list" ? ( -
- {cycles.map((cycle) => ( -
-
- handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - handleAddToFavorites={() => handleAddToFavorites(cycle)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} - /> -
-
- ))} -
- ) : viewType === "board" ? ( -
- {cycles.map((cycle) => ( - handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - handleAddToFavorites={() => handleAddToFavorites(cycle)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} - /> - ))} -
- ) : ( - - ) - ) : ( -
-
-
- - - - -
-

- {cycleTab === "all" ? "No cycles" : `No ${cycleTab} cycles`} -

- -
-
- ) - ) : viewType === "list" ? ( - - - - - - ) : viewType === "board" ? ( - - - - - - ) : ( - - - - )} - - ); -}; diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index 2404c27da35..36955398e4c 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -4,35 +4,39 @@ import { observer } from "mobx-react-lite"; // store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { CyclesBoard, CyclesList } from "components/cycles"; -import { Loader } from "@plane/ui"; +import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; +// ui components +import { Loader } from "components/ui"; +// types +import { TCycleLayout } from "types"; export interface ICyclesView { filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; - view: "list" | "board" | "gantt"; + layout: TCycleLayout; workspaceSlug: string; projectId: string; } export const CyclesView: FC = observer((props) => { - const { filter, view, workspaceSlug, projectId } = props; + const { filter, layout, workspaceSlug, projectId } = props; + // store const { cycle: cycleStore } = useMobxStore(); + // api call to fetch cycles list const { isLoading } = useSWR( - workspaceSlug && projectId ? `CYCLES_LIST_${projectId}_${filter}` : null, - workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null + workspaceSlug && projectId && filter ? `CYCLES_LIST_${projectId}_${filter}` : null, + workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null ); const cyclesList = cycleStore.cycles?.[projectId]; - console.log("cyclesList", cyclesList); return ( <> - {view === "list" && ( + {layout === "list" && ( <> {!isLoading ? ( - + ) : ( @@ -42,10 +46,11 @@ export const CyclesView: FC = observer((props) => { )} )} - {view === "board" && ( + + {layout === "board" && ( <> {!isLoading ? ( - + ) : ( @@ -55,7 +60,20 @@ export const CyclesView: FC = observer((props) => { )} )} - {view === "gantt" && } + + {layout === "gantt" && ( + <> + {!isLoading ? ( + + ) : ( + + + + + + )} + + )} ); }); diff --git a/web/components/cycles/delete-cycle-modal.tsx b/web/components/cycles/delete-cycle-modal.tsx deleted file mode 100644 index 7c219fec6f2..00000000000 --- a/web/components/cycles/delete-cycle-modal.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, { useState } from "react"; -// next -import { useRouter } from "next/router"; -// swr -import { mutate } from "swr"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// services -import { CycleService } from "services/cycle.service"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { Button } from "@plane/ui"; -// icons -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -// types -import type { IUser, ICycle, IProject } from "types"; -type TConfirmCycleDeletionProps = { - isOpen: boolean; - setIsOpen: React.Dispatch>; - data?: ICycle | null; - user: IUser | undefined; -}; -// fetch-keys -import { - COMPLETED_CYCLES_LIST, - CURRENT_CYCLE_LIST, - CYCLES_LIST, - DRAFT_CYCLES_LIST, - PROJECT_DETAILS, - UPCOMING_CYCLES_LIST, -} from "constants/fetch-keys"; -import { getDateRangeStatus } from "helpers/date-time.helper"; - -// services -const cycleService = new CycleService(); - -export const DeleteCycleModal: React.FC = ({ isOpen, setIsOpen, data, user }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const handleClose = () => { - setIsOpen(false); - setIsDeleteLoading(false); - }; - - const handleDeletion = async () => { - if (!data || !workspaceSlug || !projectId) return; - - setIsDeleteLoading(true); - - await cycleService - .deleteCycle(workspaceSlug as string, data.project, data.id, user) - .then(() => { - const cycleType = getDateRangeStatus(data.start_date, data.end_date); - const fetchKey = - cycleType === "current" - ? CURRENT_CYCLE_LIST(projectId as string) - : cycleType === "upcoming" - ? UPCOMING_CYCLES_LIST(projectId as string) - : cycleType === "completed" - ? COMPLETED_CYCLES_LIST(projectId as string) - : DRAFT_CYCLES_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => { - if (!prevData) return; - - return prevData.filter((cycle) => cycle.id !== data?.id); - }, - false - ); - - mutate( - CYCLES_LIST(projectId as string), - (prevData: any) => { - if (!prevData) return; - return prevData.filter((cycle: any) => cycle.id !== data?.id); - }, - false - ); - - // update total cycles count in the project details - mutate( - PROJECT_DETAILS(projectId.toString()), - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - total_cycles: prevData.total_cycles - 1, - }; - }, - false - ); - - handleClose(); - - setToastAlert({ - title: "Success", - type: "success", - message: "Cycle deleted successfully", - }); - }) - .catch(() => { - setIsDeleteLoading(false); - }); - }; - - return ( - - - -
- - -
-
- - -
-
-
-
-
- - Delete Cycle - -
-

- Are you sure you want to delete cycle-{" "} - {data?.name}? All of the - data related to the cycle will be permanently removed. This action cannot be undone. -

-
-
-
-
-
- - -
-
-
-
-
-
-
- ); -}; diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index b994c8f39f1..49d461d1a64 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -16,6 +16,7 @@ import { CycleGanttBlock, CycleGanttSidebarBlock } from "components/cycles"; import { ICycle } from "types"; type Props = { + workspaceSlug: string; cycles: ICycle[]; mutateCycles?: KeyedMutator; }; diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index 3f74be5001c..76f0d97735c 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -3,7 +3,6 @@ export * from "./active-cycle-details"; export * from "./active-cycle-stats"; export * from "./gantt-chart"; export * from "./cycles-view"; -export * from "./delete-cycle-modal"; export * from "./form"; export * from "./modal"; export * from "./select"; diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 676f3e06e5d..2872fd12281 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -11,7 +11,7 @@ import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; -import { DeleteCycleModal } from "components/cycles"; +import { CycleDeleteModal } from "components/cycles/cycle-delete-modal"; // ui import { CustomMenu, CustomRangeDatePicker } from "components/ui"; import { Loader, ProgressBar } from "@plane/ui"; @@ -49,7 +49,11 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = router.query as { + workspaceSlug: string; + projectId: string; + cycleId: string; + }; const { setToastAlert } = useToast(); @@ -261,7 +265,16 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu return ( <> - + {cycle && ( + setCycleDeleteModal(false)} + onSubmit={() => {}} + workspaceSlug={workspaceSlug} + projectId={projectId} + /> + )}
{ + if (typeof window === undefined || typeof window === "undefined") return null; + try { + const item = window.localStorage.getItem(key); + return item ? item : null; + } catch (error) { + window.localStorage.removeItem(key); + return null; + } +}; + +export const setLocalStorage = (key: string, value: any) => { + if (key && value) { + const _value = value ? (["string", "boolean"].includes(typeof value) ? value : JSON.stringify(value)) : null; + if (_value) window.localStorage.setItem(key, _value); + } +}; + +export const removeLocalStorage = (key: string) => { + if (key) window.localStorage.removeItem(key); +}; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index f1b7f5ea27e..0e4d3d5dbb1 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,25 +1,22 @@ -import React, { useEffect, useState } from "react"; +import { Fragment, useCallback, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Tab } from "@headlessui/react"; import useSWR from "swr"; -// hooks -import useLocalStorage from "hooks/use-local-storage"; -import useUserAuth from "hooks/use-user-auth"; +import { Plus } from "lucide-react"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; // components -import { CyclesView, ActiveCycleDetails, CreateUpdateCycleModal } from "components/cycles"; +import { CyclesView, ActiveCycleDetails } from "components/cycles"; +import { CycleCreateEditModal } from "components/cycles/cycle-create-edit-modal"; // ui import { Button } from "@plane/ui"; import { EmptyState } from "components/common"; import { Icon } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -// icons -import { PlusIcon } from "@heroicons/react/24/outline"; // images import emptyCycle from "public/empty-state/cycle.svg"; // types -import { SelectCycleType } from "types"; +import { TCycleView, TCycleLayout } from "types"; import type { NextPage } from "next"; // helper import { truncateText } from "helpers/string.helper"; @@ -27,52 +24,66 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { observer } from "mobx-react-lite"; // constants import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle"; - -type ICycleAPIFilter = "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; -type ICycleView = "list" | "board" | "gantt"; +// lib cookie +import { setLocalStorage, getLocalStorage } from "lib/local-storage"; const ProjectCyclesPage: NextPage = observer(() => { + const [createModal, setCreateModal] = useState(false); + const createOnSubmit = () => {}; + + // store + const { project: projectStore, cycle: cycleStore } = useMobxStore(); + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - // store - const { project: projectStore } = useMobxStore(); - const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; - // states - const [selectedCycle, setSelectedCycle] = useState(); - const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); - // local storage - const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "all"); - const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycle_view", "list"); - // hooks - const { user } = useUserAuth(); - // api call fetch project details + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + useSWR( workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null, - workspaceSlug && projectId - ? () => { - projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()); - } - : null + workspaceSlug && projectId ? () => projectStore.fetchProjectDetails(workspaceSlug, projectId) : null ); - /** - * Clearing form data after closing the modal - */ - useEffect(() => { - if (createUpdateCycleModal) return; + const handleCurrentLayout = useCallback( + (_layout: TCycleLayout) => { + if (projectId) { + setLocalStorage(`cycle_layout:${projectId}`, _layout); + cycleStore.setCycleLayout(_layout); + } + }, + [cycleStore, projectId] + ); - const timer = setTimeout(() => { - setSelectedCycle(undefined); - clearTimeout(timer); - }, 500); - }, [createUpdateCycleModal]); + const handleCurrentView = useCallback( + (_view: TCycleView) => { + if (projectId) { + setLocalStorage(`cycle_view:${projectId}`, _view); + cycleStore.setCycleView(_view); + if (_view === "draft" && cycleStore.cycleLayout === "gantt") { + handleCurrentLayout("list"); + } + } + }, + [cycleStore, projectId, handleCurrentLayout] + ); useEffect(() => { - if (cycleTab === "draft" && cyclesView === "gantt") { - setCyclesView("list"); + 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"); } - }, [cycleTab, cyclesView, setCyclesView]); + }, [projectId, cycleStore, handleCurrentView, handleCurrentLayout]); + + const projectDetails = projectId ? projectStore.project_details[projectId] : null; + const cycleView = cycleStore?.cycleView; + const cycleLayout = cycleStore?.cycleLayout; return ( { right={ } > - setCreateUpdateCycleModal(false)} - data={selectedCycle} - user={user} + setCreateModal(false)} + onSubmit={createOnSubmit} /> + {projectDetails?.total_cycles === 0 ? (
{ description="Cycle is a custom time period in which a team works to complete items on their backlog." image={emptyCycle} primaryButton={{ - icon: , + icon: , text: "New Cycle", onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "q", - }); - document.dispatchEvent(e); + setCreateModal(true); }, }} /> @@ -123,14 +132,10 @@ const ProjectCyclesPage: NextPage = observer(() => { i.key === cycleTab)} - selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key === cycleTab)} + defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)} + selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)} onChange={(i) => { - try { - setCycleTab(CYCLE_TAB_LIST[i].key); - } catch (e) { - setCycleTab(CYCLE_TAB_LIST[0].key); - } + handleCurrentView(CYCLE_TAB_LIST[i].key as TCycleView); }} >
@@ -148,67 +153,74 @@ const ProjectCyclesPage: NextPage = observer(() => { ))} -
- {CYCLE_VIEWS.map((view) => { - if (cycleTab === "active") return null; - if (view.key === "gantt" && cycleTab === "draft") return null; - - return ( - - ); - })} -
+ {CYCLE_VIEWS && CYCLE_VIEWS.length > 0 && cycleStore?.cycleView != "active" && ( +
+ {CYCLE_VIEWS.map((view) => { + if (view.key === "gantt" && cycleStore?.cycleView === "draft") return null; + return ( + + ); + })} +
+ )}
- + + - {cycleTab && cyclesView && workspaceSlug && projectId && ( + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} + - + + - {cycleTab && cyclesView && workspaceSlug && projectId && ( + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} + - {cycleTab && cyclesView && workspaceSlug && projectId && ( + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} + - {cycleTab && cyclesView && workspaceSlug && projectId && ( + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} diff --git a/web/store/cycle/cycles.store.ts b/web/store/cycle/cycles.store.ts index d3767c88aef..d68609a4ad2 100644 --- a/web/store/cycle/cycles.store.ts +++ b/web/store/cycle/cycles.store.ts @@ -1,7 +1,8 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; // types +import { ICycle, TCycleView, TCycleLayout, CycleDateCheckData, IIssue } from "types"; +// mobx import { RootStore } from "../root"; -import { ICycle } from "types"; // services import { ProjectService } from "services/project"; import { IssueService } from "services/issue"; @@ -11,6 +12,9 @@ export interface ICycleStore { loader: boolean; error: any | null; + cycleView: TCycleView; + cycleLayout: TCycleLayout; + cycleId: string | null; cycles: { [project_id: string]: ICycle[]; @@ -18,21 +22,31 @@ export interface ICycleStore { cycle_details: { [cycle_id: string]: ICycle; }; + active_cycle_issues: { + [cycle_id: string]: IIssue[]; + }; // computed getCycleById: (cycleId: string) => ICycle | null; // actions + setCycleView: (_cycleView: TCycleView) => void; + setCycleLayout: (_cycleLayout: TCycleLayout) => void; setCycleId: (cycleId: string) => void; + validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; + fetchCycles: ( workspaceSlug: string, projectId: string, params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" ) => Promise; fetchCycleWithId: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + fetchActiveCycleIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + createCycle: (workspaceSlug: string, projectId: string, data: any) => Promise; updateCycle: (workspaceSlug: string, projectId: string, cycleId: string, data: any) => Promise; + removeCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; @@ -42,6 +56,9 @@ export class CycleStore implements ICycleStore { loader: boolean = false; error: any | null = null; + cycleView: TCycleView = "all"; + cycleLayout: TCycleLayout = "list"; + cycleId: string | null = null; cycles: { [project_id: string]: ICycle[]; @@ -51,6 +68,10 @@ export class CycleStore implements ICycleStore { [cycle_id: string]: ICycle; } = {}; + active_cycle_issues: { + [cycle_id: string]: IIssue[]; + } = {}; + // root store rootStore; // services @@ -63,20 +84,31 @@ export class CycleStore implements ICycleStore { loader: observable, error: observable.ref, + cycleView: observable, + cycleLayout: observable, + cycleId: observable, cycles: observable.ref, cycle_details: observable.ref, + active_cycle_issues: observable.ref, // computed projectCycles: computed, // actions + setCycleView: action, + setCycleLayout: action, setCycleId: action, getCycleById: action, fetchCycles: action, fetchCycleWithId: action, + + fetchActiveCycleIssues: action, + createCycle: action, + updateCycle: action, + removeCycle: action, addCycleToFavorites: action, removeCycleFromFavorites: action, @@ -97,8 +129,18 @@ export class CycleStore implements ICycleStore { getCycleById = (cycleId: string) => this.cycle_details[cycleId] || null; // actions - setCycleId = (cycleId: string) => { - this.cycleId = cycleId; + setCycleView = (_cycleView: TCycleView) => (this.cycleView = _cycleView); + setCycleLayout = (_cycleLayout: TCycleLayout) => (this.cycleLayout = _cycleLayout); + setCycleId = (cycleId: string) => (this.cycleId = cycleId); + + validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { + try { + const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); + return response; + } catch (error) { + console.log("Failed to validate cycle dates", error); + throw error; + } }; fetchCycles = async ( @@ -112,6 +154,8 @@ export class CycleStore implements ICycleStore { const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params); + if (this.cycleView === "active") this.fetchActiveCycleIssues(workspaceSlug, projectId, cyclesResponse[0].id); + runInAction(() => { this.cycles = { ...this.cycles, @@ -144,6 +188,27 @@ export class CycleStore implements ICycleStore { } }; + fetchActiveCycleIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const _cycleIssues = await this.cycleService.getCycleIssuesWithParams(workspaceSlug, projectId, cycleId, { + priority: `urgent,high`, + }); + + const _activeCycleIssues = { + ...this.active_cycle_issues, + [cycleId]: _cycleIssues as IIssue[], + }; + + runInAction(() => { + this.active_cycle_issues = _activeCycleIssues; + }); + + return _activeCycleIssues; + } catch (error) { + console.log("error"); + } + }; + createCycle = async (workspaceSlug: string, projectId: string, data: any) => { try { const response = await this.cycleService.createCycle( @@ -154,12 +219,15 @@ export class CycleStore implements ICycleStore { ); runInAction(() => { - this.cycles = { - ...this.cycles, - [projectId]: [...this.cycles[projectId], response], + this.cycle_details = { + ...this.cycle_details, + [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"); @@ -171,23 +239,18 @@ export class CycleStore implements ICycleStore { try { const response = await this.cycleService.updateCycle(workspaceSlug, projectId, cycleId, data, undefined); - const _cycles = { - ...this.cycles, - [projectId]: this.cycles[projectId].map((cycle) => { - if (cycle.id === cycleId) return { ...cycle, ...response }; - return cycle; - }), - }; const _cycleDetails = { ...this.cycle_details, [cycleId]: { ...this.cycle_details[cycleId], ...response }, }; runInAction(() => { - this.cycles = _cycles; this.cycle_details = _cycleDetails; }); + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + return response; } catch (error) { console.log("Failed to update cycle from cycle store"); @@ -195,6 +258,43 @@ export class CycleStore implements ICycleStore { } }; + patchCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: any) => { + try { + const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data, undefined); + + const _cycleDetails = { + ...this.cycle_details, + [cycleId]: { ...this.cycle_details[cycleId], ..._response }, + }; + + runInAction(() => { + this.cycle_details = _cycleDetails; + }); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + + return _response; + } catch (error) { + console.log("Failed to patch cycle from cycle store"); + throw error; + } + }; + + removeCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId, undefined); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + + return _response; + } catch (error) { + console.log("Failed to delete cycle from cycle store"); + throw error; + } + }; + addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { try { runInAction(() => { @@ -237,12 +337,10 @@ export class CycleStore implements ICycleStore { }), }; }); - // updating through api const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); return response; } catch (error) { console.log("Failed to remove cycle from favorites - Cycle Store", error); - // resetting the local state runInAction(() => { this.cycles = { ...this.cycles, diff --git a/web/types/cycles.d.ts b/web/types/cycles.d.ts index 955e822224d..e97aa21339c 100644 --- a/web/types/cycles.d.ts +++ b/web/types/cycles.d.ts @@ -9,6 +9,10 @@ import type { IUserLite, } from "types"; +export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; + +export type TCycleLayout = "list" | "board" | "gantt"; + export interface ICycle { backlog_issues: number; cancelled_issues: number; @@ -82,9 +86,7 @@ export interface CycleIssueResponse { sub_issues_count: number; } -export type SelectCycleType = - | (ICycle & { actionType: "edit" | "delete" | "create-issue" }) - | undefined; +export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null;