diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx new file mode 100644 index 000000000..748d79371 --- /dev/null +++ b/web/components/cycles/cycles-board-card.tsx @@ -0,0 +1,366 @@ +import React, { FC } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { Disclosure, Transition } from "@headlessui/react"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { SingleProgressStats } from "components/core"; +// ui +import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui"; +import { AssigneesList } from "components/ui/avatar"; +import { RadialProgressBar } from "@plane/ui"; +// icons +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; +import { + TargetIcon, + ContrastIcon, + PersonRunningIcon, + ArrowRightIcon, + TriangleExclamationIcon, + AlarmClockIcon, +} from "components/icons"; +import { ChevronDownIcon, 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"; + +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 interface ICyclesBoardCard { + cycle: ICycle; + filter: string; +} + +export const CyclesBoardCard: FC = (props) => { + const { cycle } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // toast + 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, + })); + + const groupedIssues: any = { + backlog: cycle.backlog_issues, + unstarted: cycle.unstarted_issues, + started: cycle.started_issues, + completed: cycle.completed_issues, + cancelled: cycle.cancelled_issues, + }; + + const handleRemoveFromFavorites = () => {}; + const handleAddToFavorites = () => {}; + + const handleEditCycle = () => {}; + const handleDeleteCycle = () => {}; + + return ( +
+
+ + +
+
+ + + + + +

{truncateText(cycle.name, 15)}

+
+
+ + + {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 + )} + + {cycle.is_favorite ? ( + + ) : ( + + )} + +
+
+ {cycleStatus !== "draft" && ( + <> +
+ + {renderShortDateWithYearFormat(startDate)} +
+ +
+ + {renderShortDateWithYearFormat(endDate)} +
+ + )} +
+ +
+
+
+
Creator:
+
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.display_name} + ) : ( + + {cycle.owned_by.display_name.charAt(0)} + + )} + {cycle.owned_by.display_name} +
+
+
+
Members:
+ {cycle.assignees.length > 0 ? ( +
+ +
+ ) : ( + "No members" + )} +
+
+ +
+ {!isCompleted && ( + + )} + + + {!isCompleted && ( + + + + Delete cycle + + + )} + { + e.preventDefault(); + handleCopyText(); + }} + > + + + Copy cycle link + + + +
+
+
+
+ + +
+ + {({ open }) => ( +
+
+ Progress + + {Object.keys(groupedIssues).map((group, index) => ( + + + {group} +
+ } + completed={groupedIssues[group]} + total={cycle.total_issues} + /> + ))} +
+ } + position="bottom" + > +
+ +
+ + + + + +
+ + +
+
+
+ {stateGroups.map((group) => ( +
+
+ +
{group.title}
+
+
+ + {cycle[group.key as keyof ICycle] as number}{" "} + + -{" "} + {cycle.total_issues > 0 + ? `${Math.round( + ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 + )}%` + : "0%"} + + +
+
+ ))} +
+
+
+
+
+
+ )} + +
+ + + ); +}; diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index e69de29bb..7bbe6dae2 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -0,0 +1,53 @@ +import { FC } from "react"; +// types +import { ICycle } from "types"; +// components +import { CyclesBoardCard } from "components/cycles"; + +export interface ICyclesBoard { + cycles: ICycle[]; + filter: string; +} + +export const CyclesBoard: FC = (props) => { + const { cycles, filter } = props; + + return ( +
+ {cycles.length > 0 ? ( + <> + {cycles.map((cycle) => ( + + ))} + + ) : ( +
+
+
+ + + + +
+

{filter === "all" ? "No cycles" : `No ${filter} cycles`}

+ +
+
+ )} +
+ ); +}; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index c6ec9acec..c902f5147 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useState } from "react"; +import { FC } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks @@ -23,12 +23,12 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types import { ICycle } from "types"; -type TCycledListItem = { +type TCyclesListItem = { cycle: ICycle; - handleEditCycle: () => void; - handleDeleteCycle: () => void; - handleAddToFavorites: () => void; - handleRemoveFromFavorites: () => void; + handleEditCycle?: () => void; + handleDeleteCycle?: () => void; + handleAddToFavorites?: () => void; + handleRemoveFromFavorites?: () => void; }; const stateGroups = [ @@ -59,12 +59,12 @@ const stateGroups = [ }, ]; -export const CycledListItem: FC = (props) => { - const { cycle, handleEditCycle, handleDeleteCycle, handleAddToFavorites, handleRemoveFromFavorites } = props; +export const CyclesListItem: FC = (props) => { + const { cycle } = props; // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - + // toast const { setToastAlert } = useToast(); const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); @@ -91,6 +91,11 @@ export const CycledListItem: FC = (props) => { color: group.color, })); + const handleAddToFavorites = () => {}; + const handleRemoveFromFavorites = () => {}; + const handleEditCycle = () => {}; + const handleDeleteCycle = () => {}; + return (
@@ -246,33 +251,18 @@ export const CycledListItem: FC = (props) => { {cycle.is_favorite ? ( - ) : ( - )}
{!isCompleted && ( - { - e.preventDefault(); - handleEditCycle(); - }} - > + Edit Cycle @@ -280,24 +270,14 @@ export const CycledListItem: FC = (props) => { )} {!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 131b134b1..923bdc20a 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -3,13 +3,15 @@ import { FC } from "react"; import { Loader } from "components/ui"; // types import { ICycle } from "types"; +import { CyclesListItem } from "./cycles-list-item"; export interface ICyclesList { cycles: ICycle[]; + filter: string; } export const CyclesList: FC = (props) => { - const { cycles } = props; + const { cycles, filter } = props; return (
@@ -18,16 +20,9 @@ export const CyclesList: FC = (props) => { {cycles.length > 0 ? (
{cycles.map((cycle) => ( -
+
- handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - handleAddToFavorites={() => handleAddToFavorites(cycle)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} - /> +
))} @@ -45,7 +40,7 @@ export const CyclesList: FC = (props) => {

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

- {cycleTab && cyclesView && ( - + {cycleTab && cyclesView && workspaceSlug && projectId && ( + )} - {cycleTab && cyclesView && ( - + {cycleTab && cyclesView && workspaceSlug && projectId && ( + )} - {cycleTab && cyclesView && ( - + {cycleTab && cyclesView && workspaceSlug && projectId && ( + )} - {cycleTab && cyclesView && ( - + {cycleTab && cyclesView && workspaceSlug && projectId && ( + )} diff --git a/web/services/cycles.service.ts b/web/services/cycles.service.ts index 2b4ac7a45..55f7c5287 100644 --- a/web/services/cycles.service.ts +++ b/web/services/cycles.service.ts @@ -10,12 +10,7 @@ export class CycleService extends APIService { super(API_BASE_URL); } - async createCycle( - workspaceSlug: string, - projectId: string, - data: any, - user: ICurrentUserResponse | undefined - ): Promise { + async createCycle(workspaceSlug: string, projectId: string, data: any, user: any): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) .then((response) => { trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user); diff --git a/web/store/cycles.ts b/web/store/cycles.ts index 0ea92649a..83af1aa04 100644 --- a/web/store/cycles.ts +++ b/web/store/cycles.ts @@ -12,7 +12,9 @@ export interface ICycleStore { error: any | null; cycles: { - [project_id: string]: ICycle[]; + [project_id: string]: { + [filter_name: string]: ICycle[]; + }; }; cycle_details: { @@ -21,9 +23,11 @@ export interface ICycleStore { fetchCycles: ( workspaceSlug: string, - projectSlug: string, + projectId: string, params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" ) => Promise; + + createCycle: (workspaceSlug: string, projectId: string, data: any, filter: string) => Promise; } class CycleStore implements ICycleStore { @@ -31,7 +35,9 @@ class CycleStore implements ICycleStore { error: any | null = null; cycles: { - [project_id: string]: ICycle[]; + [project_id: string]: { + [filter_name: string]: ICycle[]; + }; } = {}; cycle_details: { @@ -56,6 +62,7 @@ class CycleStore implements ICycleStore { projectCycles: computed, // actions fetchCycles: action, + createCycle: action, }); this.rootStore = _rootStore; @@ -73,19 +80,21 @@ class CycleStore implements ICycleStore { // actions fetchCycles = async ( workspaceSlug: string, - projectSlug: string, + projectId: string, params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" ) => { try { this.loader = true; this.error = null; - const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, params); + const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params); runInAction(() => { this.cycles = { ...this.cycles, - [projectSlug]: cyclesResponse, + [projectId]: { + [params]: cyclesResponse, + }, }; this.loader = false; this.error = null; @@ -96,6 +105,32 @@ class CycleStore implements ICycleStore { this.error = error; } }; + + createCycle = async (workspaceSlug: string, projectId: string, data: any, filter: string) => { + try { + const response = await this.cycleService.createCycle( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser + ); + + runInAction(() => { + this.cycles = { + ...this.cycles, + [projectId]: { + ...this.cycles[projectId], + [filter]: [...this.cycles[projectId][filter], response], + }, + }; + }); + + return response; + } catch (error) { + console.log("Failed to create cycle from cycle store"); + throw error; + } + }; } export default CycleStore;