chore: update cycle store structure

This commit is contained in:
Aaryan Khandelwal 2023-12-15 11:43:36 +05:30
parent 8521a54e49
commit 019e2e4356
19 changed files with 507 additions and 482 deletions

View File

@ -4,8 +4,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication } from "hooks/store";
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { SingleProgressStats } from "components/core";
@ -71,20 +70,20 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const router = useRouter();
const { workspaceSlug, projectId } = props;
// store hooks
const { cycle: cycleStore } = useMobxStore();
const {
commandPalette: { toggleCreateCycleModal },
} = useApplication();
const { fetchActiveCycle, projectActiveCycle, getActiveCycleById, addCycleToFavorites, removeCycleFromFavorites } =
useCycle();
// toast alert
const { setToastAlert } = useToast();
const { isLoading } = useSWR(
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
);
const activeCycle = cycleStore.cycles?.[projectId]?.current || null;
const cycle = activeCycle ? activeCycle[0] : null;
const activeCycle = projectActiveCycle ? getActiveCycleById(projectActiveCycle) : null;
const issues = (cycleStore?.active_cycle_issues as any) || null;
// const { data: issues } = useSWR(
@ -97,14 +96,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// : null
// ) as { data: IIssue[] | undefined };
if (!cycle && isLoading)
if (!activeCycle && isLoading)
return (
<Loader>
<Loader.Item height="250px" />
</Loader>
);
if (!cycle)
if (!activeCycle)
return (
<div className="grid h-full place-items-center text-center">
<div className="space-y-2">
@ -129,24 +128,24 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div>
);
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(activeCycle.start_date ?? "");
const groupedIssues: any = {
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
started: cycle.started_issues,
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
backlog: activeCycle.backlog_issues,
unstarted: activeCycle.unstarted_issues,
started: activeCycle.started_issues,
completed: activeCycle.completed_issues,
cancelled: activeCycle.cancelled_issues,
};
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const cycleStatus = getDateRangeStatus(activeCycle.start_date, activeCycle.end_date);
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -159,7 +158,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -171,7 +170,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
value:
activeCycle.total_issues > 0
? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100
: 0,
color: group.color,
}));
@ -199,8 +201,8 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
}`}
/>
</span>
<Tooltip tooltipContent={cycle.name} position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 70)}</h3>
<Tooltip tooltipContent={activeCycle.name} position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
</Tooltip>
</span>
<span className="flex items-center gap-1 capitalize">
@ -221,19 +223,19 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
<RunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
{findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1 whitespace-nowrap">
<AlarmClock className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
{findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left
</span>
) : cycleStatus === "completed" ? (
<span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues - cycle.completed_issues > 0 && (
{activeCycle.total_issues - activeCycle.completed_issues > 0 && (
<Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
tooltipContent={`${activeCycle.total_issues - activeCycle.completed_issues} more pending ${
activeCycle.total_issues - activeCycle.completed_issues === 1 ? "issue" : "issues"
}`}
>
<span>
@ -247,7 +249,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus
)}
</span>
{cycle.is_favorite ? (
{activeCycle.is_favorite ? (
<button
onClick={(e) => {
handleRemoveFromFavorites(e);
@ -281,26 +283,26 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4">
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
{activeCycle.owned_by.avatar && activeCycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
src={activeCycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
alt={activeCycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.display_name.charAt(0)}
{activeCycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
<span className="text-custom-text-200">{activeCycle.owned_by.display_name}</span>
</div>
{cycle.assignees.length > 0 && (
{activeCycle.assignees.length > 0 && (
<div className="flex items-center gap-1 text-custom-text-200">
<AvatarGroup>
{cycle.assignees.map((assignee) => (
{activeCycle.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))}
</AvatarGroup>
@ -311,15 +313,15 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4 text-custom-text-200">
<div className="flex gap-2">
<LayersIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues
{activeCycle.total_issues} issues
</div>
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
{cycle.completed_issues} issues
{activeCycle.completed_issues} issues
</div>
</div>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${activeCycle.id}`}>
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
View Cycle
</span>
@ -350,14 +352,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div>
}
completed={groupedIssues[group]}
total={cycle.total_issues}
total={activeCycle.total_issues}
/>
))}
</div>
</div>
</div>
<div className="h-60 overflow-y-scroll border-custom-border-200">
<ActiveCycleProgressStats cycle={cycle} />
<ActiveCycleProgressStats cycle={activeCycle} />
</div>
</div>
</div>
@ -469,15 +471,18 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<span>
<LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" />
</span>
<span>Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)}</span>
<span>
Pending Issues -{" "}
{activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
</span>
</div>
</div>
<div className="relative h-64">
<ProgressChart
distribution={cycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
distribution={activeCycle.distribution?.completion_chart ?? {}}
startDate={activeCycle.start_date ?? ""}
endDate={activeCycle.end_date ?? ""}
totalIssues={activeCycle.total_issues}
/>
</div>
</div>

View File

@ -1,10 +1,8 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useCycle } from "hooks/store";
// components
import { CycleDetailsSidebar } from "./sidebar";
@ -14,14 +12,13 @@ type Props = {
};
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
// router
const router = useRouter();
const { peekCycle } = router.query;
// refs
const ref = React.useRef(null);
const { cycle: cycleStore } = useMobxStore();
const { fetchCycleWithId } = cycleStore;
// store hooks
const { fetchCycleDetails } = useCycle();
const handleClose = () => {
delete router.query.peekCycle;
@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
useEffect(() => {
if (!peekCycle) return;
fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString());
}, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]);
fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
}, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]);
return (
<>

View File

@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
@ -17,10 +18,6 @@ import {
renderShortMonthDate,
} from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
@ -28,61 +25,33 @@ import { EUserWorkspaceRoles } from "constants/workspace";
export interface ICyclesBoardCard {
workspaceSlug: string;
projectId: string;
cycle: ICycle;
cycleId: string;
}
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { setTrackElement },
user: userStore,
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
const { cycleId, workspaceSlug, projectId } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// computed
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const isDateValid = cycle.start_date || cycle.end_date;
const { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
// router
const router = useRouter();
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const cycleTotalIssues =
cycle.backlog_issues +
cycle.unstarted_issues +
cycle.started_issues +
cycle.completed_issues +
cycle.cancelled_issues;
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycle
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycle.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
// store
const {
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
// toast alert
const { setToastAlert } = useToast();
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@ -95,7 +64,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -108,7 +77,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -137,14 +106,48 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycle.id },
query: { ...query, peekCycle: cycleId },
});
};
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
const cycleStatus = getDateRangeStatus(cycleDetails.start_date, cycleDetails.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const isDateValid = cycleDetails.start_date || cycleDetails.end_date;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycleDetails
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycleDetails.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
return (
<div>
<CycleCreateUpdateModal
data={cycle}
data={cycleDetails}
isOpen={updateModal}
handleClose={() => setUpdateModal(false)}
workspaceSlug={workspaceSlug}
@ -152,22 +155,22 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
/>
<CycleDeleteModal
cycle={cycle}
cycle={cycleDetails}
isOpen={deleteModal}
handleClose={() => setDeleteModal(false)}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 truncate">
<span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span>
<Tooltip tooltipContent={cycle.name} position="top">
<span className="truncate text-base font-medium">{cycle.name}</span>
<Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycleDetails.name}</span>
</Tooltip>
</div>
<div className="flex items-center gap-2">
@ -180,7 +183,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
}}
>
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`}
</span>
)}
@ -196,11 +199,11 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
<LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount}</span>
</div>
{cycle.assignees.length > 0 && (
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
{cycleDetails.assignees.length > 0 && (
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex cursor-default items-center gap-1">
<AvatarGroup showTooltip={false}>
{cycle.assignees.map((assignee) => (
{cycleDetails.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))}
</AvatarGroup>
@ -241,7 +244,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
)}
<div className="z-10 flex items-center gap-1.5">
{isEditingAllowed &&
(cycle.is_favorite ? (
(cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button>

View File

@ -4,11 +4,9 @@ import { observer } from "mobx-react-lite";
import { useApplication } from "hooks/store";
// components
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
// types
import { ICycle } from "types";
export interface ICyclesBoard {
cycles: ICycle[];
cycleIds: string[];
filter: string;
workspaceSlug: string;
projectId: string;
@ -16,13 +14,13 @@ export interface ICyclesBoard {
}
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props;
const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
return (
<>
{cycles.length > 0 ? (
{cycleIds?.length > 0 ? (
<div className="h-full w-full">
<div className="flex h-full w-full justify-between">
<div
@ -32,8 +30,8 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
} auto-rows-max transition-all `}
>
{cycles.map((cycle) => (
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
{cycleIds.map((cycleId) => (
<CyclesBoardCard key={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
))}
</div>
<CyclePeekOverview

View File

@ -1,10 +1,8 @@
import { FC, MouseEvent, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
// stores
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
@ -20,14 +18,12 @@ import {
renderShortMonthDate,
} from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// constants
import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
type TCyclesListItem = {
cycle: ICycle;
cycleId: string;
handleEditCycle?: () => void;
handleDeleteCycle?: () => void;
handleAddToFavorites?: () => void;
@ -37,52 +33,29 @@ type TCyclesListItem = {
};
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { setTrackElement },
user: userStore,
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
const { cycleId, workspaceSlug, projectId } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// computed
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
// router
const router = useRouter();
const cycleTotalIssues =
cycle.backlog_issues +
cycle.unstarted_issues +
cycle.started_issues +
cycle.completed_issues +
cycle.cancelled_issues;
const renderDate = cycle.start_date || cycle.end_date;
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
// store hooks
const {
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
// toast alert
const { setToastAlert } = useToast();
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@ -95,7 +68,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -108,7 +81,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
@ -137,27 +110,56 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycle.id },
query: { ...query, peekCycle: cycleId },
});
};
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
const cycleStatus = getDateRangeStatus(cycleDetails.start_date, cycleDetails.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const renderDate = cycleDetails.start_date || cycleDetails.end_date;
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
return (
<>
<CycleCreateUpdateModal
data={cycle}
data={cycleDetails}
isOpen={updateModal}
handleClose={() => setUpdateModal(false)}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
<CycleDeleteModal
cycle={cycle}
cycle={cycleDetails}
isOpen={deleteModal}
handleClose={() => setDeleteModal(false)}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90">
<div className="flex w-full items-center gap-3 truncate">
<div className="flex items-center gap-4 truncate">
@ -181,8 +183,8 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span>
<Tooltip tooltipContent={cycle.name} position="top">
<span className="truncate text-base font-medium">{cycle.name}</span>
<Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycleDetails.name}</span>
</Tooltip>
</div>
</div>
@ -202,7 +204,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
}}
>
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`}
</span>
)}
@ -216,11 +218,11 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</span>
)}
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex w-16 cursor-default items-center justify-center gap-1">
{cycle.assignees.length > 0 ? (
{cycleDetails.assignees.length > 0 ? (
<AvatarGroup showTooltip={false}>
{cycle.assignees.map((assignee) => (
{cycleDetails.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))}
</AvatarGroup>
@ -232,7 +234,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</div>
</Tooltip>
{isEditingAllowed &&
(cycle.is_favorite ? (
(cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button>

View File

@ -6,18 +6,16 @@ import { useApplication } from "hooks/store";
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
// ui
import { Loader } from "@plane/ui";
// types
import { ICycle } from "types";
export interface ICyclesList {
cycles: ICycle[];
cycleIds: string[];
filter: string;
workspaceSlug: string;
projectId: string;
}
export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId } = props;
const { cycleIds, filter, workspaceSlug, projectId } = props;
// store hooks
const {
commandPalette: commandPaletteStore,
@ -26,14 +24,14 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
return (
<>
{cycles ? (
{cycleIds ? (
<>
{cycles.length > 0 ? (
{cycleIds.length > 0 ? (
<div className="h-full overflow-y-auto">
<div className="flex h-full w-full justify-between">
<div className="flex h-full w-full flex-col overflow-y-auto">
{cycles.map((cycle) => (
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
{cycleIds.map((cycleId) => (
<CyclesListItem cycleId={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} />
))}
</div>
<CyclePeekOverview

View File

@ -1,17 +1,16 @@
import { FC } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useCycle } from "hooks/store";
// components
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
// ui components
import { Loader } from "@plane/ui";
// types
import { TCycleLayout } from "types";
import { TCycleLayout, TCycleView } from "types";
export interface ICyclesView {
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
filter: TCycleView;
layout: TCycleLayout;
workspaceSlug: string;
projectId: string;
@ -20,31 +19,24 @@ export interface ICyclesView {
export const CyclesView: FC<ICyclesView> = observer((props) => {
const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
// store
const { cycle: cycleStore } = useMobxStore();
// api call to fetch cycles list
useSWR(
workspaceSlug && projectId && filter ? `CYCLES_LIST_${projectId}_${filter}` : null,
workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
);
// store hooks
const { projectCompletedCycles, projectDraftCycles, projectUpcomingCycles, projectAllCycles } = useCycle();
const cyclesList =
filter === "completed"
? cycleStore.projectCompletedCycles
? projectCompletedCycles
: filter === "draft"
? cycleStore.projectDraftCycles
: filter === "upcoming"
? cycleStore.projectUpcomingCycles
: cycleStore.projectCycles;
? projectDraftCycles
: filter === "upcoming"
? projectUpcomingCycles
: projectAllCycles;
return (
<>
{layout === "list" && (
<>
{cyclesList ? (
<CyclesList cycles={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
) : (
<Loader className="space-y-4 p-8">
<Loader.Item height="50px" />
@ -59,7 +51,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
<>
{cyclesList ? (
<CyclesBoard
cycles={cyclesList}
cycleIds={cyclesList}
filter={filter}
workspaceSlug={workspaceSlug}
projectId={projectId}
@ -78,7 +70,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
{layout === "gantt" && (
<>
{cyclesList ? (
<CyclesListGanttChartView cycles={cyclesList} workspaceSlug={workspaceSlug} />
<CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" />

View File

@ -1,17 +1,15 @@
import { Fragment, useState } from "react";
// next
import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
import { AlertTriangle } from "lucide-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// types
import { ICycle } from "types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
interface ICycleDelete {
cycle: ICycle;
@ -23,56 +21,51 @@ interface ICycleDelete {
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { postHogEventTracker },
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states
const [loader, setLoader] = useState(false);
// router
const router = useRouter();
const { cycleId, peekCycle } = router.query;
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { deleteCycle } = useCycle();
// toast alert
const { setToastAlert } = useToast();
const formSubmit = async () => {
if (!cycle) return;
setLoader(true);
if (cycle?.id)
try {
await cycleStore
.removeCycle(workspaceSlug, projectId, cycle?.id)
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle deleted successfully.",
});
postHogEventTracker("CYCLE_DELETE", {
state: "SUCCESS",
});
})
.catch(() => {
postHogEventTracker("CYCLE_DELETE", {
state: "FAILED",
});
try {
await deleteCycle(workspaceSlug, projectId, cycle.id)
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle deleted successfully.",
});
postHogEventTracker("CYCLE_DELETE", {
state: "SUCCESS",
});
})
.catch(() => {
postHogEventTracker("CYCLE_DELETE", {
state: "FAILED",
});
if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
handleClose();
} catch (error) {
setToastAlert({
type: "error",
title: "Warning!",
message: "Something went wrong please try again later.",
});
}
else
if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
handleClose();
} catch (error) {
setToastAlert({
type: "error",
title: "Warning!",
message: "Something went wrong please try again later.",
});
}
setLoader(false);
};

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { KeyedMutator } from "swr";
// hooks
import { useUser } from "hooks/store";
import { useCycle, useUser } from "hooks/store";
// services
import { CycleService } from "services/cycle.service";
// components
@ -16,7 +16,7 @@ import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
workspaceSlug: string;
cycles: ICycle[];
cycleIds: string[];
mutateCycles?: KeyedMutator<ICycle[]>;
};
@ -24,7 +24,7 @@ type Props = {
const cycleService = new CycleService();
export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { cycles, mutateCycles } = props;
const { cycleIds, mutateCycles } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@ -32,6 +32,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById } = useCycle();
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug) return;
@ -65,18 +66,21 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
};
const blockFormat = (blocks: ICycle[]) =>
blocks && blocks.length > 0
? blocks
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date))
.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.end_date ?? ""),
}))
: [];
const blockFormat = (blocks: (ICycle | null)[]) => {
if (!blocks) return [];
const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date);
const structuredBlocks = filteredBlocks.map((block) => ({
data: block,
id: block?.id ?? "",
sort_order: block?.sort_order ?? 0,
start_date: new Date(block?.start_date ?? ""),
target_date: new Date(block?.end_date ?? ""),
}));
return structuredBlocks;
};
const isAllowed =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
@ -86,7 +90,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
<GanttChartRoot
title="Cycles"
loaderTitle="Cycles"
blocks={cycles ? blockFormat(cycles) : null}
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}

View File

@ -3,8 +3,8 @@ import { Dialog, Transition } from "@headlessui/react";
// services
import { CycleService } from "services/cycle.service";
// hooks
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CycleForm } from "components/cycles";
// types
@ -23,21 +23,21 @@ const cycleService = new CycleService();
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { postHogEventTracker },
} = useMobxStore();
// states
const [activeProject, setActiveProject] = useState<string>(projectId);
// toast
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { createCycle, updateCycleDetails } = useCycle();
// toast alert
const { setToastAlert } = useToast();
const createCycle = async (payload: Partial<ICycle>) => {
const handleCreateCycle = async (payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore
.createCycle(workspaceSlug, selectedProjectId, payload)
await createCycle(workspaceSlug, selectedProjectId, payload)
.then((res) => {
setToastAlert({
type: "success",
@ -61,11 +61,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
};
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore
.patchCycle(workspaceSlug, selectedProjectId, cycleId, payload)
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
.then(() => {
setToastAlert({
type: "success",
@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}
if (isDateValid) {
if (data) await updateCycle(data.id, payload);
else await createCycle(payload);
if (data) await handleUpdateCycle(data.id, payload);
else await handleCreateCycle(payload);
handleClose();
} else
setToastAlert({

View File

@ -3,11 +3,10 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { CycleService } from "services/cycle.service";
// hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { SidebarProgressStats } from "components/core";
@ -46,19 +45,21 @@ const cycleService = new CycleService();
// TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose } = props;
// states
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
// store hooks
const {
cycle: cycleDetailsStore,
trackEvent: { setTrackElement },
user: { currentProjectRole },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails } = useCycle();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
const cycleDetails = getCycleById(cycleId);
const { setToastAlert } = useToast();
@ -74,7 +75,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const submitChanges = (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleId) return;
cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
};
const handleCopyText = () => {

View File

@ -2,8 +2,9 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useCycle } from "hooks/store";
// components
import {
CycleAppliedFiltersRoot,
@ -29,12 +30,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
projectId: string;
cycleId: string;
};
// store hooks
const {
cycle: cycleStore,
cycleIssues: { loader, getIssues, fetchIssues },
cycleIssuesFilter: { issueFilters, fetchFilters },
} = useMobxStore();
const { getCycleById } = useCycle();
useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
@ -48,7 +50,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined;
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
const cycleStatus =
cycleDetails?.start_date && cycleDetails?.end_date
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)

View File

@ -1,6 +1,11 @@
import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
// types
import { TCycleLayout, TCycleView } from "types";
export const CYCLE_TAB_LIST = [
export const CYCLE_TAB_LIST: {
key: TCycleView;
name: string;
}[] = [
{
key: "all",
name: "All",
@ -23,7 +28,11 @@ export const CYCLE_TAB_LIST = [
},
];
export const CYCLE_VIEW_LAYOUTS = [
export const CYCLE_VIEW_LAYOUTS: {
key: TCycleLayout;
icon: any;
title: string;
}[] = [
{
key: "list",
icon: List,

View File

@ -10,6 +10,7 @@ import { JoinProject } from "components/auth-screens";
import { EmptyState } from "components/common";
// images
import emptyProject from "public/empty-state/project.svg";
import { useApplication, useCycle, useModule, useProjectState, useUser } from "hooks/store";
interface IProjectAuthWrapper {
children: ReactNode;
@ -19,18 +20,22 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const { children } = props;
// store
const {
user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
project: { fetchProjectDetails, workspaceProjects },
projectLabel: { fetchProjectLabels },
projectMember: { fetchProjectMembers },
projectState: { fetchProjectStates },
projectEstimates: { fetchProjectEstimates },
cycle: { fetchCycles },
module: { fetchModules },
projectViews: { fetchAllViews },
inbox: { fetchInboxesList, isInboxEnabled },
commandPalette: { toggleCreateProjectModal },
} = useMobxStore();
const {
commandPalette: { toggleCreateProjectModal },
} = useApplication();
const {
membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
} = useUser();
const { fetchAllCycles } = useCycle();
const { fetchModules } = useModule();
const { fetchProjectStates } = useProjectState();
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -68,7 +73,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// fetching project cycles
useSWR(
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") : null
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null
);
// fetching project modules
useSWR(
@ -80,7 +85,6 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null
);
// TODO: fetching project pages
// fetching project inboxes if inbox is enabled
useSWR(
workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null,

View File

@ -1,9 +1,8 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useCycle } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// layouts
import { AppLayout } from "layouts/app-layout";
@ -19,24 +18,23 @@ import emptyCycle from "public/empty-state/cycle.svg";
import { NextPageWithLayout } from "types/app";
const CycleDetailPage: NextPageWithLayout = () => {
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const { cycle: cycleStore } = useMobxStore();
// store hooks
const { fetchCycleDetails } = useCycle();
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const { error } = useSWR(
workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null,
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
workspaceSlug && projectId && cycleId
? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
: null
);
const toggleSidebar = () => {
setValue(`${!isSidebarCollapsed}`);
};
const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
return (
<>

View File

@ -1,15 +1,17 @@
import { Fragment, useCallback, useEffect, useState, ReactElement } from "react";
import { Fragment, useCallback, useState, ReactElement } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
import { Plus } from "lucide-react";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useCycle, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { CyclesHeader } from "components/headers";
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
import { NewEmptyState } from "components/common/new-empty-state";
// ui
import { Tooltip } from "@plane/ui";
// images
@ -20,64 +22,37 @@ import { NextPageWithLayout } from "types/app";
// constants
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
// lib cookie
import { setLocalStorage, getLocalStorage } from "lib/local-storage";
import { NewEmptyState } from "components/common/new-empty-state";
// TODO: use-local-storage hook instead of lib file.
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false);
// store
// store hooks
const {
cycle: cycleStore,
user: { currentProjectRole },
} = useMobxStore();
const { projectCycles } = cycleStore;
membership: { currentProjectRole },
} = useUser();
const { projectAllCycles } = useCycle();
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
// local storage
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
const handleCurrentLayout = useCallback(
(_layout: TCycleLayout) => {
if (projectId) {
setLocalStorage(`cycle_layout:${projectId}`, _layout);
cycleStore.setCycleLayout(_layout);
}
setCycleLayout(_layout);
},
[cycleStore, projectId]
[setCycleLayout]
);
const handleCurrentView = useCallback(
(_view: TCycleView) => {
if (projectId) {
setLocalStorage(`cycle_view:${projectId}`, _view);
cycleStore.setCycleView(_view);
if (_view === "draft" && cycleStore.cycleLayout === "gantt") {
handleCurrentLayout("list");
}
}
setCycleTab(_view);
if (_view === "draft") handleCurrentLayout("list");
},
[cycleStore, projectId, handleCurrentLayout]
[handleCurrentLayout, setCycleTab]
);
useEffect(() => {
if (projectId) {
const _viewKey = `cycle_view:${projectId}`;
const _viewValue = getLocalStorage(_viewKey);
if (_viewValue && _viewValue !== cycleStore?.cycleView) cycleStore.setCycleView(_viewValue as TCycleView);
else handleCurrentView("all");
const _layoutKey = `cycle_layout:${projectId}`;
const _layoutValue = getLocalStorage(_layoutKey);
if (_layoutValue && _layoutValue !== cycleStore?.cycleView)
cycleStore.setCycleLayout(_layoutValue as TCycleLayout);
else handleCurrentLayout("list");
}
}, [projectId, cycleStore, handleCurrentView, handleCurrentLayout]);
const cycleView = cycleStore?.cycleView;
const cycleLayout = cycleStore?.cycleLayout;
const totalCycles = projectCycles?.length ?? 0;
const totalCycles = projectAllCycles?.length ?? 0;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@ -117,11 +92,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tab.Group
as="div"
className="flex h-full flex-col overflow-hidden"
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)}
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)}
onChange={(i) => {
handleCurrentView(CYCLE_TAB_LIST[i].key as TCycleView);
}}
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
>
<div className="flex flex-col items-end justify-between gap-4 border-b border-custom-border-200 px-4 pb-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll">
@ -138,26 +111,24 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab>
))}
</Tab.List>
{cycleStore?.cycleView != "active" && (
{cycleTab !== "active" && (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => {
if (layout.key === "gantt" && cycleStore?.cycleView === "draft") return null;
if (layout.key === "gantt" && cycleTab === "draft") return null;
return (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
cycleStore?.cycleLayout == layout.key
? "bg-custom-background-100 shadow-custom-shadow-2xs"
: ""
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${
cycleStore?.cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>
@ -170,10 +141,10 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && (
{cycleTab && cycleLayout && (
<CyclesView
filter={"all"}
layout={cycleLayout as TCycleLayout}
filter="all"
layout={cycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
@ -186,9 +157,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && (
{cycleTab && cycleLayout && (
<CyclesView
filter={"upcoming"}
filter="upcoming"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
@ -198,9 +169,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && workspaceSlug && projectId && (
{cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView
filter={"completed"}
filter="completed"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
@ -210,9 +181,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleView && cycleLayout && workspaceSlug && projectId && (
{cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView
filter={"draft"}
filter="draft"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}

View File

@ -11,7 +11,7 @@ export class CycleService extends APIService {
super(API_BASE_URL);
}
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<any> {
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<ICycle> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
.then((response) => response?.data)
.catch((error) => {
@ -22,8 +22,8 @@ export class CycleService extends APIService {
async getCyclesWithParams(
workspaceSlug: string,
projectId: string,
cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
): Promise<ICycle[]> {
cycleType?: "current"
): Promise<Record<string, ICycle>> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, {
params: {
cycle_view: cycleType,

View File

@ -1,7 +1,8 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import set from "lodash/set";
import { set, omit } from "lodash";
import { isFuture, isPast } from "date-fns";
// types
import { ICycle, TCycleView, CycleDateCheckData } from "types";
import { ICycle, CycleDateCheckData } from "types";
// mobx
import { RootStore } from "store/root.store";
// services
@ -10,40 +11,30 @@ import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service";
export interface ICycleStore {
// states
loader: boolean;
error: any | null;
cycleView: TCycleView;
cycleId: string | null;
// observables
cycleMap: {
[projectId: string]: {
[cycleId: string]: ICycle;
};
[cycleId: string]: ICycle;
};
cycles: {
[projectId: string]: {
[filterType: string]: string[];
};
activeCycleMap: {
[cycleId: string]: ICycle;
};
// computed
getCycleById: (cycleId: string) => ICycle | null;
projectCycles: string[] | null;
projectAllCycles: string[] | null;
projectCompletedCycles: string[] | null;
projectUpcomingCycles: string[] | null;
projectDraftCycles: string[] | null;
projectActiveCycle: string | null;
// computed actions
getCycleById: (cycleId: string) => ICycle | null;
getActiveCycleById: (cycleId: string) => ICycle | null;
// actions
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
fetchCycles: (
workspaceSlug: string,
projectId: string,
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
) => Promise<void>;
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise<Record<string, ICycle>>;
fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise<Record<string, ICycle>>;
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
updateCycleDetails: (
workspaceSlug: string,
@ -52,29 +43,19 @@ export interface ICycleStore {
data: Partial<ICycle>
) => Promise<ICycle>;
deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
}
export class CycleStore implements ICycleStore {
// states
loader: boolean = false;
error: any | null = null;
cycleView: TCycleView = "all";
cycleId: string | null = null;
// observables
cycleMap: {
[projectId: string]: {
[cycleId: string]: ICycle;
};
[cycleId: string]: ICycle;
} = {};
cycles: {
[projectId: string]: {
[filterType: string]: string[];
};
} = {};
activeCycleMap: { [cycleId: string]: ICycle } = {};
// root store
rootStore;
// services
@ -84,29 +65,28 @@ export class CycleStore implements ICycleStore {
constructor(_rootStore: RootStore) {
makeObservable(this, {
loader: observable,
// states
loader: observable.ref,
error: observable.ref,
cycleId: observable.ref,
// observables
cycleMap: observable,
cycles: observable,
activeCycleMap: observable,
// computed
projectCycles: computed,
projectAllCycles: computed,
projectCompletedCycles: computed,
projectUpcomingCycles: computed,
projectDraftCycles: computed,
// actions
projectActiveCycle: computed,
// computed actions
getCycleById: action,
fetchCycles: action,
getActiveCycleById: action,
// actions
fetchAllCycles: action,
fetchActiveCycle: action,
fetchCycleDetails: action,
createCycle: action,
updateCycleDetails: action,
deleteCycle: action,
addCycleToFavorites: action,
removeCycleFromFavorites: action,
});
@ -118,46 +98,86 @@ export class CycleStore implements ICycleStore {
}
// computed
get projectCycles() {
get projectAllCycles() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
return this.cycles[projectId]?.all || null;
const allCycles = Object.keys(this.cycleMap ?? {}).filter(
(cycleId) => this.cycleMap?.[cycleId]?.project === projectId
);
return allCycles || null;
}
get projectCompletedCycles() {
const projectId = this.rootStore.app.router.projectId;
const allCycles = this.projectAllCycles;
if (!projectId) return null;
if (!allCycles) return null;
return this.cycles[projectId]?.completed || null;
const completedCycles = allCycles.filter((cycleId) => {
const hasEndDatePassed = isPast(new Date(this.cycleMap?.[cycleId]?.end_date ?? ""));
return hasEndDatePassed;
});
return completedCycles || null;
}
get projectUpcomingCycles() {
const projectId = this.rootStore.app.router.projectId;
const allCycles = this.projectAllCycles;
if (!projectId) return null;
if (!allCycles) return null;
return this.cycles[projectId]?.upcoming || null;
const upcomingCycles = allCycles.filter((cycleId) => {
const isStartDateUpcoming = isFuture(new Date(this.cycleMap?.[cycleId]?.start_date ?? ""));
return isStartDateUpcoming;
});
return upcomingCycles || null;
}
get projectDraftCycles() {
const projectId = this.rootStore.app.router.projectId;
const allCycles = this.projectAllCycles;
if (!projectId) return null;
if (!allCycles) return null;
return this.cycles[projectId]?.draft || null;
const draftCycles = allCycles.filter((cycleId) => {
const cycleDetails = this.cycleMap?.[cycleId];
return !cycleDetails?.start_date && !cycleDetails?.end_date;
});
return draftCycles || null;
}
getCycleById = (cycleId: string) => {
get projectActiveCycle() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
return this.cycleMap?.[projectId]?.[cycleId] || null;
};
const activeCycle = Object.keys(this.activeCycleMap ?? {}).find(
(cycleId) => this.activeCycleMap?.[cycleId]?.project === projectId
);
return activeCycle || null;
}
/**
* @description returns cycle details by cycle id
* @param cycleId
* @returns
*/
getCycleById = (cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null;
/**
* @description returns active cycle details by cycle id
* @param cycleId
* @returns
*/
getActiveCycleById = (cycleId: string): ICycle | null => this.activeCycleMap?.[cycleId] ?? null;
// actions
validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => {
try {
const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
@ -168,27 +188,52 @@ export class CycleStore implements ICycleStore {
}
};
fetchCycles = async (
workspaceSlug: string,
projectId: string,
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
) => {
fetchAllCycles = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params);
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId);
runInAction(() => {
set(this.cycleMap, [projectId], cyclesResponse);
set(this.cycles, [projectId, params], Object.keys(cyclesResponse));
Object.values(cyclesResponse).forEach((cycle) => {
set(this.cycleMap, [cycle.id], cycle);
});
this.loader = false;
this.error = null;
});
return cyclesResponse;
} catch (error) {
console.error("Failed to fetch project cycles in project store", error);
this.loader = false;
this.error = error;
throw error;
}
};
fetchActiveCycle = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, "current");
runInAction(() => {
Object.values(cyclesResponse).forEach((cycle) => {
set(this.activeCycleMap, [cycle.id], cycle);
});
this.loader = false;
this.error = null;
});
return cyclesResponse;
} catch (error) {
this.loader = false;
this.error = error;
throw error;
}
};
@ -197,7 +242,8 @@ export class CycleStore implements ICycleStore {
const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId);
runInAction(() => {
set(this.cycleMap, [projectId, response?.id], response);
set(this.cycleMap, [response.id], { ...this.cycleMap?.[response.id], ...response });
set(this.activeCycleMap, [response.id], { ...this.activeCycleMap?.[response.id], ...response });
});
return response;
@ -212,12 +258,10 @@ export class CycleStore implements ICycleStore {
const response = await this.cycleService.createCycle(workspaceSlug, projectId, data);
runInAction(() => {
set(this.cycleMap, [projectId, response?.id], response);
set(this.cycleMap, [response.id], response);
set(this.activeCycleMap, [response.id], response);
});
const _currentView = this.cycleView === "active" ? "current" : this.cycleView;
this.fetchCycles(workspaceSlug, projectId, _currentView);
return response;
} catch (error) {
console.log("Failed to create cycle from cycle store");
@ -227,18 +271,14 @@ export class CycleStore implements ICycleStore {
updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>) => {
try {
const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data);
const currentCycle = this.cycleMap[projectId][cycleId];
const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data);
runInAction(() => {
set(this.cycleMap, [projectId, cycleId], { ...currentCycle, ...data });
set(this.cycleMap, [cycleId], { ...this.cycleMap?.[cycleId], ...data });
set(this.activeCycleMap, [cycleId], { ...this.activeCycleMap?.[cycleId], ...data });
});
const _currentView = this.cycleView === "active" ? "current" : this.cycleView;
this.fetchCycles(workspaceSlug, projectId, _currentView);
return _response;
return response;
} catch (error) {
console.log("Failed to patch cycle from cycle store");
throw error;
@ -246,32 +286,36 @@ export class CycleStore implements ICycleStore {
};
deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try {
if (!this.cycleMap?.[projectId]?.[cycleId]) return;
const originalCycle = this.cycleMap[cycleId];
const originalActiveCycle = this.activeCycleMap[cycleId];
try {
runInAction(() => {
delete this.cycleMap[projectId][cycleId];
omit(this.cycleMap, [cycleId]);
omit(this.activeCycleMap, [cycleId]);
});
const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId);
return _response;
await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId);
} catch (error) {
console.log("Failed to delete cycle from cycle store");
const _currentView = this.cycleView === "active" ? "current" : this.cycleView;
this.fetchCycles(workspaceSlug, projectId, _currentView);
runInAction(() => {
set(this.cycleMap, [cycleId], originalCycle);
set(this.activeCycleMap, [cycleId], originalActiveCycle);
});
throw error;
}
};
addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try {
const currentCycle = this.cycleMap[projectId][cycleId];
if (currentCycle.is_favorite) return;
const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], true);
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true);
});
// updating through api.
@ -279,10 +323,12 @@ export class CycleStore implements ICycleStore {
return response;
} catch (error) {
console.log("Failed to add cycle to favorites in the cycles store", error);
const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], false);
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false);
});
throw error;
@ -291,22 +337,24 @@ export class CycleStore implements ICycleStore {
removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try {
const currentCycle = this.cycleMap[projectId][cycleId];
if (!currentCycle.is_favorite) return;
const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], false);
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false);
});
const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId);
return response;
} catch (error) {
console.log("Failed to remove cycle from favorites - Cycle Store", error);
const currentCycle = this.getCycleById(cycleId);
const currentActiveCycle = this.getActiveCycleById(cycleId);
runInAction(() => {
set(this.cycleMap, [projectId, cycleId, "is_favorite"], true);
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true);
});
throw error;

View File

@ -1,5 +1,5 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import set from "lodash/set";
import { set } from "lodash";
// services
import { ProjectService } from "services/project";
import { ModuleService } from "services/module.service";