refactor: cycles store (#2716)

* refactor: cycles store

* refactor: active cycle details
This commit is contained in:
Aaryan Khandelwal 2023-11-09 18:37:45 +05:30 committed by GitHub
parent 162faf8339
commit 884b219508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 200 additions and 285 deletions

View File

@ -75,12 +75,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { isLoading } = useSWR( useSWR(
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
); );
const activeCycle = cycleStore.cycles?.[projectId] || null; const activeCycle = cycleStore.cycles?.[projectId]?.active || null;
const cycle = activeCycle ? activeCycle[0] : null; const cycle = activeCycle ? activeCycle[0] : null;
const issues = (cycleStore?.active_cycle_issues as any) || null; const issues = (cycleStore?.active_cycle_issues as any) || null;
@ -94,7 +94,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// : null // : null
// ) as { data: IIssue[] | undefined }; // ) as { data: IIssue[] | undefined };
if (isLoading) if (!cycle)
return ( return (
<Loader> <Loader>
<Loader.Item height="250px" /> <Loader.Item height="250px" />
@ -143,7 +143,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -156,7 +156,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -87,7 +87,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -100,7 +100,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -12,7 +12,7 @@ export interface ICyclesBoard {
filter: string; filter: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
peekCycle: string; peekCycle: string | undefined;
} }
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => { export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {

View File

@ -1 +0,0 @@
export const CycleGantt = () => <></>;

View File

@ -87,7 +87,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -100,7 +100,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -15,7 +15,7 @@ export interface ICyclesView {
layout: TCycleLayout; layout: TCycleLayout;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
peekCycle: string; peekCycle: string | undefined;
} }
export const CyclesView: FC<ICyclesView> = observer((props) => { export const CyclesView: FC<ICyclesView> = observer((props) => {
@ -30,7 +30,14 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
); );
const cyclesList = cycleStore.cycles?.[projectId]; const cyclesList =
filter === "completed"
? cycleStore.projectCompletedCycles
: filter === "draft"
? cycleStore.projectDraftCycles
: filter === "upcoming"
? cycleStore.projectUpcomingCycles
: cycleStore.projectCycles;
return ( return (
<> <>

View File

@ -5,7 +5,6 @@ export * from "./gantt-chart";
export * from "./cycles-view"; export * from "./cycles-view";
export * from "./form"; export * from "./form";
export * from "./modal"; export * from "./modal";
export * from "./select";
export * from "./sidebar"; export * from "./sidebar";
export * from "./transfer-issues-modal"; export * from "./transfer-issues-modal";
export * from "./transfer-issues"; export * from "./transfer-issues";
@ -13,7 +12,6 @@ export * from "./cycles-list";
export * from "./cycles-list-item"; export * from "./cycles-list-item";
export * from "./cycles-board"; export * from "./cycles-board";
export * from "./cycles-board-card"; export * from "./cycles-board-card";
export * from "./cycles-gantt";
export * from "./delete-modal"; export * from "./delete-modal";
export * from "./cycle-peek-overview"; export * from "./cycle-peek-overview";
export * from "./cycles-list-item"; export * from "./cycles-list-item";

View File

@ -50,7 +50,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => const updateCycle = async (cycleId: string, payload: Partial<ICycle>) =>
cycleStore cycleStore
.updateCycle(workspaceSlug, projectId, cycleId, payload) .patchCycle(workspaceSlug, projectId, cycleId, payload)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",

View File

@ -1,122 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Listbox, Transition } from "@headlessui/react";
// icons
import { ContrastIcon } from "@plane/ui";
import { Plus } from "lucide-react";
// services
import { CycleService } from "services/cycle.service";
// components
import { CycleCreateUpdateModal } from "components/cycles";
// fetch-keys
import { CYCLES_LIST } from "constants/fetch-keys";
export type IssueCycleSelectProps = {
projectId: string;
value: any;
onChange: (value: any) => void;
multiple?: boolean;
};
const cycleService = new CycleService();
export const CycleSelect: React.FC<IssueCycleSelectProps> = ({ projectId, value, onChange, multiple = false }) => {
// states
const [isCycleModalActive, setCycleModalActive] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: cycles } = useSWR(
workspaceSlug && projectId ? CYCLES_LIST(projectId) : null,
workspaceSlug && projectId
? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "all")
: null
);
const options = cycles?.map((cycle) => ({ value: cycle.id, display: cycle.name }));
const openCycleModal = () => {
setCycleModalActive(true);
};
const closeCycleModal = () => {
setCycleModalActive(false);
};
return (
<>
{workspaceSlug && projectId && (
<CycleCreateUpdateModal
isOpen={isCycleModalActive}
handleClose={closeCycleModal}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
<Listbox as="div" className="relative" value={value} onChange={onChange} multiple={multiple}>
{({ open }) => (
<>
<Listbox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-300 hover:bg-custom-background-90 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
>
<ContrastIcon className="h-3 w-3 text-custom-text-200" />
<div className="flex items-center gap-2 truncate">
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-custom-background-80 shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`}
>
<div className="py-1">
{options ? (
options.length > 0 ? (
options.map((option) => (
<Listbox.Option
key={option.value}
className={({ selected, active }) =>
`${
selected || (Array.isArray(value) ? value.includes(option.value) : value === option.value)
? "bg-indigo-50 font-medium"
: ""
} ${
active ? "bg-indigo-50" : ""
} relative cursor-pointer select-none p-2 text-custom-text-100`
}
value={option.value}
>
<span className={` flex items-center gap-2 truncate`}>{option.display}</span>
</Listbox.Option>
))
) : (
<p className="text-center text-sm text-custom-text-200">No options</p>
)
) : (
<p className="text-center text-sm text-custom-text-200">Loading...</p>
)}
<button
type="button"
className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-custom-text-100"
onClick={openCycleModal}
>
<Plus className="h-4 w-4 text-gray-400" aria-hidden="true" />
<span>Create cycle</span>
</button>
</div>
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
</>
);
};

View File

@ -502,7 +502,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
<div className="relative h-40 w-80"> <div className="relative h-40 w-80">
<ProgressChart <ProgressChart
distribution={cycleDetails.distribution.completion_chart} distribution={cycleDetails.distribution?.completion_chart ?? {}}
startDate={cycleDetails.start_date ?? ""} startDate={cycleDetails.start_date ?? ""}
endDate={cycleDetails.end_date ?? ""} endDate={cycleDetails.end_date ?? ""}
totalIssues={cycleDetails.total_issues} totalIssues={cycleDetails.total_issues}
@ -512,7 +512,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
) : ( ) : (
"" ""
)} )}
{cycleDetails.total_issues > 0 && ( {cycleDetails.total_issues > 0 && cycleDetails.distribution && (
<div className="h-full w-full pt-5 border-t border-custom-border-200"> <div className="h-full w-full pt-5 border-t border-custom-border-200">
<SidebarProgressStats <SidebarProgressStats
distribution={cycleDetails.distribution} distribution={cycleDetails.distribution}

View File

@ -103,7 +103,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
[issueFilterStore, projectId, workspaceSlug] [issueFilterStore, projectId, workspaceSlug]
); );
const cyclesList = projectId ? cycleStore.cycles[projectId.toString()] : undefined; const cyclesList = cycleStore.projectCycles;
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
return ( return (

View File

@ -34,7 +34,7 @@ export const IssueCycleSelect: React.FC<IssueCycleSelectProps> = observer((props
if (workspaceSlug && projectId) cycleStore.fetchCycles(workspaceSlug, projectId, "all"); if (workspaceSlug && projectId) cycleStore.fetchCycles(workspaceSlug, projectId, "all");
}; };
const cycles = projectId ? cycleStore.cycles[projectId] : undefined; const cycles = cycleStore.projectCycles;
const selectedCycle = cycles ? cycles?.find((i) => i.id === value) : undefined; const selectedCycle = cycles ? cycles?.find((i) => i.id === value) : undefined;

View File

@ -2,20 +2,18 @@ import React, { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { PencilIcon, StarIcon, TrashIcon } from "lucide-react";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu, PhotoFilterIcon } from "@plane/ui";
// icons
import { PencilIcon, Sparkles, StarIcon, TrashIcon } from "lucide-react";
// types
import { IProjectView } from "types";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { calculateTotalFilters } from "helpers/filter.helper"; import { calculateTotalFilters } from "helpers/filter.helper";
// types
import { IProjectView } from "types";
type Props = { type Props = {
view: IProjectView; view: IProjectView;
@ -64,7 +62,7 @@ export const ProjectViewListItem: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="grid place-items-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100"> <div className="grid place-items-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100">
<Sparkles size={14} strokeWidth={2} /> <PhotoFilterIcon className="h-3.5 w-3.5" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<p className="truncate text-sm leading-4 font-medium">{truncateText(view.name, 75)}</p> <p className="truncate text-sm leading-4 font-medium">{truncateText(view.name, 75)}</p>

View File

@ -1,14 +1,12 @@
import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useState } from "react"; import { Pencil, Trash2 } from "lucide-react";
// components // components
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu, PhotoFilterIcon } from "@plane/ui";
// icons
import { Pencil, Sparkles, Trash2 } from "lucide-react";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { calculateTotalFilters } from "helpers/filter.helper"; import { calculateTotalFilters } from "helpers/filter.helper";
@ -38,7 +36,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="grid place-items-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100"> <div className="grid place-items-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100">
<Sparkles size={14} strokeWidth={2} /> <PhotoFilterIcon className="h-3.5 w-3.5" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<p className="truncate text-sm leading-4 font-medium">{truncateText(view.name, 75)}</p> <p className="truncate text-sm leading-4 font-medium">{truncateText(view.name, 75)}</p>

View File

@ -133,18 +133,13 @@ export const formatDateDistance = (date: string | Date) => {
export const getDateRangeStatus = (startDate: string | null | undefined, endDate: string | null | undefined) => { export const getDateRangeStatus = (startDate: string | null | undefined, endDate: string | null | undefined) => {
if (!startDate || !endDate) return "draft"; if (!startDate || !endDate) return "draft";
const today = renderDateFormat(new Date()); const now = new Date();
const now = new Date(today);
const start = new Date(startDate); const start = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
if (start <= now && end >= now) { if (start <= now && end >= now) return "current";
return "current"; else if (start > now) return "upcoming";
} else if (start > now) { else return "completed";
return "upcoming";
} else {
return "completed";
}
}; };
export const renderShortDateWithYearFormat = (date: string | Date, placeholder?: string) => { export const renderShortDateWithYearFormat = (date: string | Date, placeholder?: string) => {

View File

@ -1,8 +1,7 @@
import { Fragment, useCallback, useEffect, useState, ReactElement } from "react"; import { Fragment, useCallback, useEffect, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Tab } from "@headlessui/react";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
@ -31,16 +30,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const { currentProjectDetails } = projectStore; const { currentProjectDetails } = projectStore;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query as { const { workspaceSlug, projectId, peekCycle } = router.query;
workspaceSlug: string;
projectId: string;
peekCycle: string;
};
// fetching project details
useSWR(
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
workspaceSlug && projectId ? () => projectStore.fetchProjectDetails(workspaceSlug, projectId) : null
);
const handleCurrentLayout = useCallback( const handleCurrentLayout = useCallback(
(_layout: TCycleLayout) => { (_layout: TCycleLayout) => {
@ -83,11 +73,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const cycleView = cycleStore?.cycleView; const cycleView = cycleStore?.cycleView;
const cycleLayout = cycleStore?.cycleLayout; const cycleLayout = cycleStore?.cycleLayout;
if (!workspaceSlug || !projectId) return null;
return ( return (
<> <>
<CycleCreateUpdateModal <CycleCreateUpdateModal
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId.toString()}
isOpen={createModal} isOpen={createModal}
handleClose={() => setCreateModal(false)} handleClose={() => setCreateModal(false)}
/> />
@ -163,29 +155,29 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && workspaceSlug && projectId && ( {cycleView && cycleLayout && (
<CyclesView <CyclesView
filter={"all"} filter={"all"}
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId.toString()}
peekCycle={peekCycle} peekCycle={peekCycle?.toString()}
/> />
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto"> <Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
<ActiveCycleDetails workspaceSlug={workspaceSlug} projectId={projectId} /> <ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && workspaceSlug && projectId && ( {cycleView && cycleLayout && (
<CyclesView <CyclesView
filter={"upcoming"} filter={"upcoming"}
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId.toString()}
peekCycle={peekCycle} peekCycle={peekCycle?.toString()}
/> />
)} )}
</Tab.Panel> </Tab.Panel>
@ -195,9 +187,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<CyclesView <CyclesView
filter={"completed"} filter={"completed"}
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId.toString()}
peekCycle={peekCycle} peekCycle={peekCycle?.toString()}
/> />
)} )}
</Tab.Panel> </Tab.Panel>
@ -207,9 +199,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<CyclesView <CyclesView
filter={"draft"} filter={"draft"}
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId.toString()}
peekCycle={peekCycle} peekCycle={peekCycle?.toString()}
/> />
)} )}
</Tab.Panel> </Tab.Panel>

View File

@ -71,23 +71,6 @@ export class CycleService extends APIService {
}); });
} }
async updateCycle(
workspaceSlug: string,
projectId: string,
cycleId: string,
data: any,
user: IUser | undefined
): Promise<any> {
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data)
.then((response) => {
trackEventService.trackCycleEvent(response?.data, "CYCLE_UPDATE", user as IUser);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async patchCycle( async patchCycle(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -97,7 +80,7 @@ export class CycleService extends APIService {
): Promise<any> { ): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data)
.then((response) => { .then((response) => {
trackEventService.trackCycleEvent(response?.data, "CYCLE_UPDATE", user as IUser); if (user) trackEventService.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -133,7 +133,9 @@ export class CycleIssueFilterStore implements ICycleIssueFilterStore {
}, },
}; };
await this.cycleService.updateCycle(workspaceSlug, projectId, cycleId, payload, undefined); const user = this.rootStore.user.currentUser ?? undefined;
await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, payload, user);
} catch (error) { } catch (error) {
this.fetchCycleFilters(workspaceSlug, projectId, cycleId); this.fetchCycleFilters(workspaceSlug, projectId, cycleId);

View File

@ -7,6 +7,7 @@ import { RootStore } from "../root";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
import { getDateRangeStatus } from "helpers/date-time.helper";
export interface ICycleStore { export interface ICycleStore {
loader: boolean; loader: boolean;
@ -17,17 +18,23 @@ export interface ICycleStore {
cycleId: string | null; cycleId: string | null;
cycles: { cycles: {
[project_id: string]: ICycle[]; [projectId: string]: {
[filterType: string]: ICycle[];
};
}; };
cycle_details: { cycle_details: {
[cycle_id: string]: ICycle; [cycleId: string]: ICycle;
}; };
active_cycle_issues: { active_cycle_issues: {
[cycle_id: string]: IIssue[]; [cycleId: string]: IIssue[];
}; };
// computed // computed
getCycleById: (cycleId: string) => ICycle | null; getCycleById: (cycleId: string) => ICycle | null;
projectCycles: ICycle[] | null;
projectCompletedCycles: ICycle[] | null;
projectUpcomingCycles: ICycle[] | null;
projectDraftCycles: ICycle[] | null;
// actions // actions
setCycleView: (_cycleView: TCycleView) => void; setCycleView: (_cycleView: TCycleView) => void;
@ -44,12 +51,12 @@ export interface ICycleStore {
fetchCycleWithId: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>; fetchCycleWithId: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
fetchActiveCycleIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>; fetchActiveCycleIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
createCycle: (workspaceSlug: string, projectId: string, data: any) => Promise<ICycle>; createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
updateCycle: (workspaceSlug: string, projectId: string, cycleId: string, data: any) => Promise<ICycle>; patchCycle: (workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>) => Promise<ICycle>;
removeCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>; removeCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>; addCycleToFavorites: (workspaceSlug: string, projectId: string, cycle: ICycle) => Promise<any>;
removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>; removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycle: ICycle) => Promise<void>;
} }
export class CycleStore implements ICycleStore { export class CycleStore implements ICycleStore {
@ -61,15 +68,17 @@ export class CycleStore implements ICycleStore {
cycleId: string | null = null; cycleId: string | null = null;
cycles: { cycles: {
[project_id: string]: ICycle[]; [projectId: string]: {
[filterType: string]: ICycle[];
};
} = {}; } = {};
cycle_details: { cycle_details: {
[cycle_id: string]: ICycle; [cycleId: string]: ICycle;
} = {}; } = {};
active_cycle_issues: { active_cycle_issues: {
[cycle_id: string]: IIssue[]; [cycleId: string]: IIssue[];
} = {}; } = {};
// root store // root store
@ -94,6 +103,9 @@ export class CycleStore implements ICycleStore {
// computed // computed
projectCycles: computed, projectCycles: computed,
projectCompletedCycles: computed,
projectUpcomingCycles: computed,
projectDraftCycles: computed,
// actions // actions
setCycleView: action, setCycleView: action,
@ -107,7 +119,6 @@ export class CycleStore implements ICycleStore {
fetchActiveCycleIssues: action, fetchActiveCycleIssues: action,
createCycle: action, createCycle: action,
updateCycle: action,
removeCycle: action, removeCycle: action,
addCycleToFavorites: action, addCycleToFavorites: action,
@ -122,8 +133,34 @@ export class CycleStore implements ICycleStore {
// computed // computed
get projectCycles() { get projectCycles() {
if (!this.rootStore.project.projectId) return null; const projectId = this.rootStore.project.projectId;
return this.cycles[this.rootStore.project.projectId] || null;
if (!projectId) return null;
return this.cycles[projectId]?.all || null;
}
get projectCompletedCycles() {
const projectId = this.rootStore.project.projectId;
if (!projectId) return null;
return this.cycles[projectId]?.completed || null;
}
get projectUpcomingCycles() {
const projectId = this.rootStore.project.projectId;
if (!projectId) return null;
return this.cycles[projectId]?.upcoming || null;
}
get projectDraftCycles() {
const projectId = this.rootStore.project.projectId;
if (!projectId) return null;
return this.cycles[projectId]?.draft || null;
} }
getCycleById = (cycleId: string) => this.cycle_details[cycleId] || null; getCycleById = (cycleId: string) => this.cycle_details[cycleId] || null;
@ -159,7 +196,7 @@ export class CycleStore implements ICycleStore {
runInAction(() => { runInAction(() => {
this.cycles = { this.cycles = {
...this.cycles, ...this.cycles,
[projectId]: cyclesResponse, [projectId]: { ...this.cycles[projectId], [params]: cyclesResponse },
}; };
this.loader = false; this.loader = false;
this.error = null; this.error = null;
@ -209,16 +246,14 @@ export class CycleStore implements ICycleStore {
} }
}; };
createCycle = async (workspaceSlug: string, projectId: string, data: any) => { createCycle = async (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => {
try { try {
console.log("Cycle Creating");
const response = await this.cycleService.createCycle( const response = await this.cycleService.createCycle(
workspaceSlug, workspaceSlug,
projectId, projectId,
data, data,
this.rootStore.user.currentUser this.rootStore.user.currentUser
); );
console.log("Cycle created");
runInAction(() => { runInAction(() => {
this.cycle_details = { this.cycle_details = {
@ -237,30 +272,7 @@ export class CycleStore implements ICycleStore {
} }
}; };
updateCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: any) => { patchCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>) => {
try {
const response = await this.cycleService.updateCycle(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 update cycle from cycle store");
throw error;
}
};
patchCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: any) => {
try { try {
const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data, undefined); const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data, undefined);
@ -297,30 +309,60 @@ export class CycleStore implements ICycleStore {
} }
}; };
addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycle: ICycle) => {
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const statusCyclesList = this.cycles[projectId]?.[cycleStatus] ?? [];
const allCyclesList = this.projectCycles ?? [];
try { try {
runInAction(() => { runInAction(() => {
this.cycles = { this.cycles = {
...this.cycles, ...this.cycles,
[projectId]: this.cycles[projectId].map((cycle) => { [projectId]: {
if (cycle.id === cycleId) return { ...cycle, is_favorite: true }; ...this.cycles[projectId],
return cycle; [cycleStatus]: statusCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: true };
return c;
}), }),
all: allCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: true };
return c;
}),
},
};
this.cycle_details = {
...this.cycle_details,
[cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: true },
}; };
}); });
// updating through api. // updating through api.
const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }); const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycle.id });
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to add cycle to favorites in the cycles store", error); console.log("Failed to add cycle to favorites in the cycles store", error);
// resetting the local state
// reset on error
runInAction(() => { runInAction(() => {
this.cycles = { this.cycles = {
...this.cycles, ...this.cycles,
[projectId]: this.cycles[projectId].map((cycle) => { [projectId]: {
if (cycle.id === cycleId) return { ...cycle, is_favorite: false }; ...this.cycles[projectId],
return cycle; [cycleStatus]: statusCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: false };
return c;
}), }),
all: allCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: false };
return c;
}),
},
};
this.cycle_details = {
...this.cycle_details,
[cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: false },
}; };
}); });
@ -328,30 +370,62 @@ export class CycleStore implements ICycleStore {
} }
}; };
removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycle: ICycle) => {
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const statusCyclesList = this.cycles[projectId]?.[cycleStatus] ?? [];
const allCyclesList = this.projectCycles ?? [];
try { try {
runInAction(() => { runInAction(() => {
this.cycles = { this.cycles = {
...this.cycles, ...this.cycles,
[projectId]: this.cycles[projectId].map((cycle) => { [projectId]: {
if (cycle.id === cycleId) return { ...cycle, is_favorite: false }; ...this.cycles[projectId],
return cycle; [cycleStatus]: statusCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: false };
return c;
}), }),
all: allCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: false };
return c;
}),
},
};
this.cycle_details = {
...this.cycle_details,
[cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: false },
}; };
}); });
const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId);
const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycle.id);
return response; return response;
} catch (error) { } catch (error) {
console.log("Failed to remove cycle from favorites - Cycle Store", error); console.log("Failed to remove cycle from favorites - Cycle Store", error);
// reset on error
runInAction(() => { runInAction(() => {
this.cycles = { this.cycles = {
...this.cycles, ...this.cycles,
[projectId]: this.cycles[projectId].map((cycle) => { [projectId]: {
if (cycle.id === cycleId) return { ...cycle, is_favorite: true }; ...this.cycles[projectId],
return cycle; [cycleStatus]: statusCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: true };
return c;
}), }),
all: allCyclesList?.map((c) => {
if (c.id === cycle.id) return { ...c, is_favorite: true };
return c;
}),
},
};
this.cycle_details = {
...this.cycle_details,
[cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: true },
}; };
}); });
throw error; throw error;
} }
}; };

13
web/types/cycles.d.ts vendored
View File

@ -1,13 +1,4 @@
import type { import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types";
IUser,
IIssue,
IProject,
IProjectLite,
IWorkspace,
IWorkspaceLite,
IIssueFilterOptions,
IUserLite,
} from "types";
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
@ -20,7 +11,7 @@ export interface ICycle {
created_at: Date; created_at: Date;
created_by: string; created_by: string;
description: string; description: string;
distribution: { distribution?: {
assignees: TAssigneesDistribution[]; assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution; completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[]; labels: TLabelsDistribution[];