From f22705846dd2a9a81fd4cec41459aa9f7f96b798 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 29 Sep 2023 17:32:47 +0530 Subject: [PATCH] chore: refactoring cycles list --- web/components/cycles/cycles-board.tsx | 0 web/components/cycles/cycles-gantt.tsx | 0 web/components/cycles/cycles-list-item.tsx | 316 ++++++++++++++++++ web/components/cycles/cycles-list.tsx | 87 +++-- web/components/cycles/cycles-view-legacy.tsx | 254 ++++++++++++++ web/components/cycles/cycles-view.tsx | 271 ++------------- web/components/cycles/form.tsx | 97 +++--- web/components/cycles/index.ts | 5 +- web/components/cycles/modal.tsx | 36 +- web/hooks/use-local-storage.tsx | 4 +- .../projects/[projectId]/cycles/index.tsx | 19 +- 11 files changed, 732 insertions(+), 357 deletions(-) create mode 100644 web/components/cycles/cycles-board.tsx create mode 100644 web/components/cycles/cycles-gantt.tsx create mode 100644 web/components/cycles/cycles-list-item.tsx create mode 100644 web/components/cycles/cycles-view-legacy.tsx diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/web/components/cycles/cycles-gantt.tsx b/web/components/cycles/cycles-gantt.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx new file mode 100644 index 000000000..c6ec9acec --- /dev/null +++ b/web/components/cycles/cycles-list-item.tsx @@ -0,0 +1,316 @@ +import { FC, useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { RadialProgressBar } from "@plane/ui"; +import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui"; +// icons +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; +import { + TargetIcon, + ContrastIcon, + PersonRunningIcon, + ArrowRightIcon, + TriangleExclamationIcon, + AlarmClockIcon, +} from "components/icons"; +import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline"; +// helpers +import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; +import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +// types +import { ICycle } from "types"; + +type TCycledListItem = { + cycle: ICycle; + handleEditCycle: () => void; + handleDeleteCycle: () => void; + handleAddToFavorites: () => void; + handleRemoveFromFavorites: () => void; +}; + +const stateGroups = [ + { + key: "backlog_issues", + title: "Backlog", + color: "#dee2e6", + }, + { + key: "unstarted_issues", + title: "Unstarted", + color: "#26b5ce", + }, + { + key: "started_issues", + title: "Started", + color: "#f7ae59", + }, + { + key: "cancelled_issues", + title: "Cancelled", + color: "#d687ff", + }, + { + key: "completed_issues", + title: "Completed", + color: "#09a953", + }, +]; + +export const CycledListItem: FC = (props) => { + const { cycle, handleEditCycle, handleDeleteCycle, handleAddToFavorites, handleRemoveFromFavorites } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const isCompleted = cycleStatus === "completed"; + const endDate = new Date(cycle.end_date ?? ""); + const startDate = new Date(cycle.start_date ?? ""); + + const handleCopyText = () => { + const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Cycle link copied to clipboard.", + }); + }); + }; + + const progressIndicatorData = stateGroups.map((group, index) => ({ + id: index, + name: group.title, + value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, + color: group.color, + })); + + return ( +
+
+ + +
+
+
+ +
+ +

{truncateText(cycle.name, 60)}

+
+

{cycle.description}

+
+
+
+ + {cycleStatus === "current" ? ( + + + {findHowManyDaysLeft(cycle.end_date ?? new Date())} days left + + ) : cycleStatus === "upcoming" ? ( + + + {findHowManyDaysLeft(cycle.start_date ?? new Date())} days left + + ) : cycleStatus === "completed" ? ( + + {cycle.total_issues - cycle.completed_issues > 0 && ( + + + + + + )}{" "} + Completed + + ) : ( + cycleStatus + )} + + + {cycleStatus !== "draft" && ( +
+
+ + {renderShortDateWithYearFormat(startDate)} +
+ +
+ + {renderShortDateWithYearFormat(endDate)} +
+
+ )} + +
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.display_name} + ) : ( + + {cycle.owned_by.display_name.charAt(0)} + + )} +
+ + Progress + +
+ } + > + + {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.is_favorite ? ( + + ) : ( + + )} +
+ + {!isCompleted && ( + { + e.preventDefault(); + handleEditCycle(); + }} + > + + + Edit Cycle + + + )} + {!isCompleted && ( + { + e.preventDefault(); + handleDeleteCycle(); + }} + > + + + Delete cycle + + + )} + { + e.preventDefault(); + handleCopyText(); + }} + > + + + Copy cycle link + + + +
+
+
+
+ + +
+ + ); +}; diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 02fcf11c0..131b134b1 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,30 +1,75 @@ -import { useRouter } from "next/router"; import { FC } from "react"; -import useSWR from "swr"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { CyclesView } from "./cycles-view"; +// ui +import { Loader } from "components/ui"; +// types +import { ICycle } from "types"; export interface ICyclesList { - filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; + cycles: ICycle[]; } export const CyclesList: FC = (props) => { - const { filter } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - // store - const { cycle: cycleStore } = useMobxStore(); + const { cycles } = props; - useSWR( - workspaceSlug && projectId ? `CYCLES_LIST_${projectId}` : null, - workspaceSlug && projectId - ? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), filter) - : null + return ( +
+ {cycles ? ( + <> + {cycles.length > 0 ? ( +
+ {cycles.map((cycle) => ( +
+
+ handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + handleAddToFavorites={() => handleAddToFavorites(cycle)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} + /> +
+
+ ))} +
+ ) : ( +
+
+
+ + + + +
+

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

+ +
+
+ )} + + ) : ( + + + + + + )} +
); - if (!projectId) { - return <>; - } - return ; }; diff --git a/web/components/cycles/cycles-view-legacy.tsx b/web/components/cycles/cycles-view-legacy.tsx new file mode 100644 index 000000000..5d025e4fd --- /dev/null +++ b/web/components/cycles/cycles-view-legacy.tsx @@ -0,0 +1,254 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { KeyedMutator, mutate } from "swr"; +// services +import cyclesService from "services/cycles.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 "components/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"; +import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle"; + +type Props = { + cycles: ICycle[] | undefined; + mutateCycles?: KeyedMutator; + viewType: string | null; +}; + +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 + ); + + cyclesService + .addCycleToFavorites(workspaceSlug as string, projectId as string, { + cycle: cycle.id, + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = (cycle: ICycle) => { + if (!workspaceSlug || !projectId) return; + + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const fetchKey = + cycleStatus === "current" + ? 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 + ); + + cyclesService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the cycle from favorites. Please try again.", + }); + }); + }; + + return ( + <> + 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 9d469c7ea..3de52fd41 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -1,257 +1,38 @@ -import React, { useState } from "react"; - import { useRouter } from "next/router"; - -import { KeyedMutator, mutate } from "swr"; - -// services -import cyclesService from "services/cycles.service"; -// hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; -import useLocalStorage from "hooks/use-local-storage"; +import { FC } from "react"; +import useSWR from "swr"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; // components -import { - CreateUpdateCycleModal, - CyclesListGanttChartView, - DeleteCycleModal, - SingleCycleCard, - SingleCycleList, -} from "components/cycles"; -// ui -import { Loader } from "components/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"; -import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle"; +import { CyclesList } from "components/cycles"; -type Props = { - cycles: ICycle[] | undefined; - mutateCycles?: KeyedMutator; - viewType: string | null; -}; - -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); +export interface ICyclesView { + filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; + view: "list" | "board" | "gantt"; +} +export const CyclesView: FC = (props) => { + const { filter, view } = props; + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { cycle: cycleStore } = useMobxStore(); - 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 - ); - - cyclesService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = (cycle: ICycle) => { - if (!workspaceSlug || !projectId) return; - - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); - - const fetchKey = - cycleStatus === "current" - ? 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 - ); - - cyclesService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", - }); - }); - }; - + useSWR( + workspaceSlug && projectId ? `CYCLES_LIST_${projectId}` : null, + workspaceSlug && projectId + ? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), filter) + : null + ); + if (!projectId) { + return <>; + } 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" ? ( - - - - - - ) : ( - - - - )} + {view === "list" && } + {view === "board" && } + {view === "gantt" && } ); }; diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 4643a58bc..8ec1ab7e2 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,50 +1,33 @@ -import { useEffect } from "react"; - -// react-hook-form import { Controller, useForm } from "react-hook-form"; - // ui -import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; +import { Input, TextArea } from "@plane/ui"; +import { DateSelect, PrimaryButton, SecondaryButton } from "components/ui"; // types import { ICycle } from "types"; type Props = { handleFormSubmit: (values: Partial) => Promise; handleClose: () => void; - status: boolean; data?: ICycle | null; }; -const defaultValues: Partial = { - name: "", - description: "", - start_date: null, - end_date: null, -}; - -export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { +export const CycleForm: React.FC = (props) => { + const { handleFormSubmit, handleClose, data } = props; + // form data const { - register, formState: { errors, isSubmitting }, handleSubmit, control, - reset, watch, } = useForm({ - defaultValues, + defaultValues: { + name: data?.name || "", + description: data?.description || "", + start_date: data?.start_date || null, + end_date: data?.end_date || null, + }, }); - const handleCreateUpdateCycle = async (formData: Partial) => { - await handleFormSubmit(formData); - }; - - useEffect(() => { - reset({ - ...defaultValues, - ...data, - }); - }, [data, reset]); - const startDate = watch("start_date"); const endDate = watch("end_date"); @@ -55,39 +38,50 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat maxDate?.setDate(maxDate.getDate() - 1); return ( -
+
-

- {status ? "Update" : "Create"} Cycle -

+

{status ? "Update" : "Create"} Cycle

- ( + + )} />
-