mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: update cycle store structure
This commit is contained in:
parent
8521a54e49
commit
019e2e4356
@ -4,8 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useApplication, useCycle } from "hooks/store";
|
||||||
import { useApplication } from "hooks/store";
|
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { SingleProgressStats } from "components/core";
|
import { SingleProgressStats } from "components/core";
|
||||||
@ -71,20 +70,20 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { cycle: cycleStore } = useMobxStore();
|
|
||||||
const {
|
const {
|
||||||
commandPalette: { toggleCreateCycleModal },
|
commandPalette: { toggleCreateCycleModal },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { fetchActiveCycle, projectActiveCycle, getActiveCycleById, addCycleToFavorites, removeCycleFromFavorites } =
|
||||||
|
useCycle();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { isLoading } = useSWR(
|
const { isLoading } = useSWR(
|
||||||
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
|
workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
|
||||||
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
|
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeCycle = cycleStore.cycles?.[projectId]?.current || null;
|
const activeCycle = projectActiveCycle ? getActiveCycleById(projectActiveCycle) : 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;
|
||||||
|
|
||||||
// const { data: issues } = useSWR(
|
// const { data: issues } = useSWR(
|
||||||
@ -97,14 +96,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
// : null
|
// : null
|
||||||
// ) as { data: IIssue[] | undefined };
|
// ) as { data: IIssue[] | undefined };
|
||||||
|
|
||||||
if (!cycle && isLoading)
|
if (!activeCycle && isLoading)
|
||||||
return (
|
return (
|
||||||
<Loader>
|
<Loader>
|
||||||
<Loader.Item height="250px" />
|
<Loader.Item height="250px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!cycle)
|
if (!activeCycle)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full place-items-center text-center">
|
<div className="grid h-full place-items-center text-center">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -129,24 +128,24 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const endDate = new Date(cycle.end_date ?? "");
|
const endDate = new Date(activeCycle.end_date ?? "");
|
||||||
const startDate = new Date(cycle.start_date ?? "");
|
const startDate = new Date(activeCycle.start_date ?? "");
|
||||||
|
|
||||||
const groupedIssues: any = {
|
const groupedIssues: any = {
|
||||||
backlog: cycle.backlog_issues,
|
backlog: activeCycle.backlog_issues,
|
||||||
unstarted: cycle.unstarted_issues,
|
unstarted: activeCycle.unstarted_issues,
|
||||||
started: cycle.started_issues,
|
started: activeCycle.started_issues,
|
||||||
completed: cycle.completed_issues,
|
completed: activeCycle.completed_issues,
|
||||||
cancelled: cycle.cancelled_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>) => {
|
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -159,7 +158,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).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -171,7 +170,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
const progressIndicatorData = stateGroups.map((group, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
name: group.title,
|
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,
|
color: group.color,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -199,8 +201,8 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={cycle.name} position="top-left">
|
<Tooltip tooltipContent={activeCycle.name} position="top-left">
|
||||||
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 70)}</h3>
|
<h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 capitalize">
|
<span className="flex items-center gap-1 capitalize">
|
||||||
@ -221,19 +223,19 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
{cycleStatus === "current" ? (
|
{cycleStatus === "current" ? (
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
<span className="flex gap-1 whitespace-nowrap">
|
||||||
<RunningIcon className="h-4 w-4" />
|
<RunningIcon className="h-4 w-4" />
|
||||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
{findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left
|
||||||
</span>
|
</span>
|
||||||
) : cycleStatus === "upcoming" ? (
|
) : cycleStatus === "upcoming" ? (
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
<span className="flex gap-1 whitespace-nowrap">
|
||||||
<AlarmClock className="h-4 w-4" />
|
<AlarmClock className="h-4 w-4" />
|
||||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
{findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left
|
||||||
</span>
|
</span>
|
||||||
) : cycleStatus === "completed" ? (
|
) : cycleStatus === "completed" ? (
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
<span className="flex gap-1 whitespace-nowrap">
|
||||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
{activeCycle.total_issues - activeCycle.completed_issues > 0 && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
|
tooltipContent={`${activeCycle.total_issues - activeCycle.completed_issues} more pending ${
|
||||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
activeCycle.total_issues - activeCycle.completed_issues === 1 ? "issue" : "issues"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@ -247,7 +249,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
cycleStatus
|
cycleStatus
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{cycle.is_favorite ? (
|
{activeCycle.is_favorite ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
handleRemoveFromFavorites(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-4">
|
||||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
<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
|
<img
|
||||||
src={cycle.owned_by.avatar}
|
src={activeCycle.owned_by.avatar}
|
||||||
height={16}
|
height={16}
|
||||||
width={16}
|
width={16}
|
||||||
className="rounded-full"
|
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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{cycle.assignees.length > 0 && (
|
{activeCycle.assignees.length > 0 && (
|
||||||
<div className="flex items-center gap-1 text-custom-text-200">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<AvatarGroup>
|
<AvatarGroup>
|
||||||
{cycle.assignees.map((assignee) => (
|
{activeCycle.assignees.map((assignee) => (
|
||||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
||||||
))}
|
))}
|
||||||
</AvatarGroup>
|
</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 items-center gap-4 text-custom-text-200">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<LayersIcon className="h-4 w-4 flex-shrink-0" />
|
<LayersIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
{cycle.total_issues} issues
|
{activeCycle.total_issues} issues
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
|
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
|
||||||
{cycle.completed_issues} issues
|
{activeCycle.completed_issues} issues
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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
|
View Cycle
|
||||||
</span>
|
</span>
|
||||||
@ -350,14 +352,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
completed={groupedIssues[group]}
|
completed={groupedIssues[group]}
|
||||||
total={cycle.total_issues}
|
total={activeCycle.total_issues}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-60 overflow-y-scroll border-custom-border-200">
|
<div className="h-60 overflow-y-scroll border-custom-border-200">
|
||||||
<ActiveCycleProgressStats cycle={cycle} />
|
<ActiveCycleProgressStats cycle={activeCycle} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -469,15 +471,18 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
<span>
|
<span>
|
||||||
<LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" />
|
<LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" />
|
||||||
</span>
|
</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>
|
</div>
|
||||||
<div className="relative h-64">
|
<div className="relative h-64">
|
||||||
<ProgressChart
|
<ProgressChart
|
||||||
distribution={cycle.distribution?.completion_chart ?? {}}
|
distribution={activeCycle.distribution?.completion_chart ?? {}}
|
||||||
startDate={cycle.start_date ?? ""}
|
startDate={activeCycle.start_date ?? ""}
|
||||||
endDate={cycle.end_date ?? ""}
|
endDate={activeCycle.end_date ?? ""}
|
||||||
totalIssues={cycle.total_issues}
|
totalIssues={activeCycle.total_issues}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// hooks
|
||||||
|
import { useCycle } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CycleDetailsSidebar } from "./sidebar";
|
import { CycleDetailsSidebar } from "./sidebar";
|
||||||
|
|
||||||
@ -14,14 +12,13 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
|
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { peekCycle } = router.query;
|
const { peekCycle } = router.query;
|
||||||
|
// refs
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
|
// store hooks
|
||||||
const { cycle: cycleStore } = useMobxStore();
|
const { fetchCycleDetails } = useCycle();
|
||||||
|
|
||||||
const { fetchCycleWithId } = cycleStore;
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
delete router.query.peekCycle;
|
delete router.query.peekCycle;
|
||||||
@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peekCycle) return;
|
if (!peekCycle) return;
|
||||||
fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString());
|
fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
||||||
}, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]);
|
}, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useApplication, useCycle, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||||
@ -17,10 +18,6 @@ import {
|
|||||||
renderShortMonthDate,
|
renderShortMonthDate,
|
||||||
} from "helpers/date-time.helper";
|
} from "helpers/date-time.helper";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
|
||||||
import { ICycle } from "types";
|
|
||||||
// store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
@ -28,61 +25,33 @@ import { EUserWorkspaceRoles } from "constants/workspace";
|
|||||||
export interface ICyclesBoardCard {
|
export interface ICyclesBoardCard {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
cycle: ICycle;
|
cycleId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||||
const { cycle, workspaceSlug, projectId } = props;
|
const { cycleId, workspaceSlug, projectId } = props;
|
||||||
// store
|
|
||||||
const {
|
|
||||||
cycle: cycleStore,
|
|
||||||
trackEvent: { setTrackElement },
|
|
||||||
user: userStore,
|
|
||||||
} = useMobxStore();
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// states
|
// states
|
||||||
const [updateModal, setUpdateModal] = useState(false);
|
const [updateModal, setUpdateModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
// computed
|
// router
|
||||||
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;
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// store
|
||||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
const {
|
||||||
|
eventTracker: { setTrackElement },
|
||||||
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
} = useApplication();
|
||||||
|
const {
|
||||||
const cycleTotalIssues =
|
membership: { currentProjectRole },
|
||||||
cycle.backlog_issues +
|
} = useUser();
|
||||||
cycle.unstarted_issues +
|
const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
|
||||||
cycle.started_issues +
|
// toast alert
|
||||||
cycle.completed_issues +
|
const { setToastAlert } = useToast();
|
||||||
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";
|
|
||||||
|
|
||||||
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
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({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link Copied!",
|
title: "Link Copied!",
|
||||||
@ -95,7 +64,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).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -108,7 +77,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).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -137,14 +106,48 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CycleCreateUpdateModal
|
<CycleCreateUpdateModal
|
||||||
data={cycle}
|
data={cycleDetails}
|
||||||
isOpen={updateModal}
|
isOpen={updateModal}
|
||||||
handleClose={() => setUpdateModal(false)}
|
handleClose={() => setUpdateModal(false)}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
@ -152,22 +155,22 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CycleDeleteModal
|
<CycleDeleteModal
|
||||||
cycle={cycle}
|
cycle={cycleDetails}
|
||||||
isOpen={deleteModal}
|
isOpen={deleteModal}
|
||||||
handleClose={() => setDeleteModal(false)}
|
handleClose={() => setDeleteModal(false)}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
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 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 justify-between gap-2">
|
||||||
<div className="flex items-center gap-3 truncate">
|
<div className="flex items-center gap-3 truncate">
|
||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={cycle.name} position="top">
|
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
||||||
<span className="truncate text-base font-medium">{cycle.name}</span>
|
<span className="truncate text-base font-medium">{cycleDetails.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -180,7 +183,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentCycle.value === "current"
|
{currentCycle.value === "current"
|
||||||
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
|
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
|
||||||
: `${currentCycle.label}`}
|
: `${currentCycle.label}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -196,11 +199,11 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||||
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
||||||
</div>
|
</div>
|
||||||
{cycle.assignees.length > 0 && (
|
{cycleDetails.assignees.length > 0 && (
|
||||||
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
||||||
<div className="flex cursor-default items-center gap-1">
|
<div className="flex cursor-default items-center gap-1">
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{cycle.assignees.map((assignee) => (
|
{cycleDetails.assignees.map((assignee) => (
|
||||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
||||||
))}
|
))}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
@ -241,7 +244,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="z-10 flex items-center gap-1.5">
|
<div className="z-10 flex items-center gap-1.5">
|
||||||
{isEditingAllowed &&
|
{isEditingAllowed &&
|
||||||
(cycle.is_favorite ? (
|
(cycleDetails.is_favorite ? (
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -4,11 +4,9 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||||
// types
|
|
||||||
import { ICycle } from "types";
|
|
||||||
|
|
||||||
export interface ICyclesBoard {
|
export interface ICyclesBoard {
|
||||||
cycles: ICycle[];
|
cycleIds: string[];
|
||||||
filter: string;
|
filter: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -16,13 +14,13 @@ export interface ICyclesBoard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
|
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
|
||||||
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props;
|
const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cycles.length > 0 ? (
|
{cycleIds?.length > 0 ? (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<div className="flex h-full w-full justify-between">
|
<div className="flex h-full w-full justify-between">
|
||||||
<div
|
<div
|
||||||
@ -32,8 +30,8 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
|
|||||||
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
|
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
|
||||||
} auto-rows-max transition-all `}
|
} auto-rows-max transition-all `}
|
||||||
>
|
>
|
||||||
{cycles.map((cycle) => (
|
{cycleIds.map((cycleId) => (
|
||||||
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
|
<CyclesBoardCard key={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<CyclePeekOverview
|
<CyclePeekOverview
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { FC, MouseEvent, useState } from "react";
|
import { FC, MouseEvent, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// stores
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useApplication, useCycle, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||||
@ -20,14 +18,12 @@ import {
|
|||||||
renderShortMonthDate,
|
renderShortMonthDate,
|
||||||
} from "helpers/date-time.helper";
|
} from "helpers/date-time.helper";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
|
||||||
import { ICycle } from "types";
|
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type TCyclesListItem = {
|
type TCyclesListItem = {
|
||||||
cycle: ICycle;
|
cycleId: string;
|
||||||
handleEditCycle?: () => void;
|
handleEditCycle?: () => void;
|
||||||
handleDeleteCycle?: () => void;
|
handleDeleteCycle?: () => void;
|
||||||
handleAddToFavorites?: () => void;
|
handleAddToFavorites?: () => void;
|
||||||
@ -37,52 +33,29 @@ type TCyclesListItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||||
const { cycle, workspaceSlug, projectId } = props;
|
const { cycleId, workspaceSlug, projectId } = props;
|
||||||
// store
|
|
||||||
const {
|
|
||||||
cycle: cycleStore,
|
|
||||||
trackEvent: { setTrackElement },
|
|
||||||
user: userStore,
|
|
||||||
} = useMobxStore();
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// states
|
// states
|
||||||
const [updateModal, setUpdateModal] = useState(false);
|
const [updateModal, setUpdateModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
// computed
|
// router
|
||||||
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;
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// store hooks
|
||||||
const cycleTotalIssues =
|
const {
|
||||||
cycle.backlog_issues +
|
eventTracker: { setTrackElement },
|
||||||
cycle.unstarted_issues +
|
} = useApplication();
|
||||||
cycle.started_issues +
|
const {
|
||||||
cycle.completed_issues +
|
membership: { currentProjectRole },
|
||||||
cycle.cancelled_issues;
|
} = useUser();
|
||||||
|
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
|
||||||
const renderDate = cycle.start_date || cycle.end_date;
|
// toast alert
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
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);
|
|
||||||
|
|
||||||
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
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({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link Copied!",
|
title: "Link Copied!",
|
||||||
@ -95,7 +68,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).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -108,7 +81,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).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -137,27 +110,56 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<CycleCreateUpdateModal
|
<CycleCreateUpdateModal
|
||||||
data={cycle}
|
data={cycleDetails}
|
||||||
isOpen={updateModal}
|
isOpen={updateModal}
|
||||||
handleClose={() => setUpdateModal(false)}
|
handleClose={() => setUpdateModal(false)}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
<CycleDeleteModal
|
<CycleDeleteModal
|
||||||
cycle={cycle}
|
cycle={cycleDetails}
|
||||||
isOpen={deleteModal}
|
isOpen={deleteModal}
|
||||||
handleClose={() => setDeleteModal(false)}
|
handleClose={() => setDeleteModal(false)}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
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="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 w-full items-center gap-3 truncate">
|
||||||
<div className="flex items-center gap-4 truncate">
|
<div className="flex items-center gap-4 truncate">
|
||||||
@ -181,8 +183,8 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={cycle.name} position="top">
|
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
||||||
<span className="truncate text-base font-medium">{cycle.name}</span>
|
<span className="truncate text-base font-medium">{cycleDetails.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -202,7 +204,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentCycle.value === "current"
|
{currentCycle.value === "current"
|
||||||
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
|
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
|
||||||
: `${currentCycle.label}`}
|
: `${currentCycle.label}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -216,11 +218,11 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
</span>
|
</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">
|
<div className="flex w-16 cursor-default items-center justify-center gap-1">
|
||||||
{cycle.assignees.length > 0 ? (
|
{cycleDetails.assignees.length > 0 ? (
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{cycle.assignees.map((assignee) => (
|
{cycleDetails.assignees.map((assignee) => (
|
||||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
||||||
))}
|
))}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
@ -232,7 +234,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isEditingAllowed &&
|
{isEditingAllowed &&
|
||||||
(cycle.is_favorite ? (
|
(cycleDetails.is_favorite ? (
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,18 +6,16 @@ import { useApplication } from "hooks/store";
|
|||||||
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
|
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
|
||||||
import { ICycle } from "types";
|
|
||||||
|
|
||||||
export interface ICyclesList {
|
export interface ICyclesList {
|
||||||
cycles: ICycle[];
|
cycleIds: string[];
|
||||||
filter: string;
|
filter: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesList: FC<ICyclesList> = observer((props) => {
|
export const CyclesList: FC<ICyclesList> = observer((props) => {
|
||||||
const { cycles, filter, workspaceSlug, projectId } = props;
|
const { cycleIds, filter, workspaceSlug, projectId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
commandPalette: commandPaletteStore,
|
commandPalette: commandPaletteStore,
|
||||||
@ -26,14 +24,14 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cycles ? (
|
{cycleIds ? (
|
||||||
<>
|
<>
|
||||||
{cycles.length > 0 ? (
|
{cycleIds.length > 0 ? (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="flex h-full w-full justify-between">
|
<div className="flex h-full w-full justify-between">
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||||
{cycles.map((cycle) => (
|
{cycleIds.map((cycleId) => (
|
||||||
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
<CyclesListItem cycleId={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<CyclePeekOverview
|
<CyclePeekOverview
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// store
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useCycle } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
|
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
|
||||||
// ui components
|
// ui components
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TCycleLayout } from "types";
|
import { TCycleLayout, TCycleView } from "types";
|
||||||
|
|
||||||
export interface ICyclesView {
|
export interface ICyclesView {
|
||||||
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
|
filter: TCycleView;
|
||||||
layout: TCycleLayout;
|
layout: TCycleLayout;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -20,31 +19,24 @@ export interface ICyclesView {
|
|||||||
|
|
||||||
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||||
const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
|
const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
|
||||||
|
// store hooks
|
||||||
// store
|
const { projectCompletedCycles, projectDraftCycles, projectUpcomingCycles, projectAllCycles } = useCycle();
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
const cyclesList =
|
const cyclesList =
|
||||||
filter === "completed"
|
filter === "completed"
|
||||||
? cycleStore.projectCompletedCycles
|
? projectCompletedCycles
|
||||||
: filter === "draft"
|
: filter === "draft"
|
||||||
? cycleStore.projectDraftCycles
|
? projectDraftCycles
|
||||||
: filter === "upcoming"
|
: filter === "upcoming"
|
||||||
? cycleStore.projectUpcomingCycles
|
? projectUpcomingCycles
|
||||||
: cycleStore.projectCycles;
|
: projectAllCycles;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{layout === "list" && (
|
{layout === "list" && (
|
||||||
<>
|
<>
|
||||||
{cyclesList ? (
|
{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 className="space-y-4 p-8">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
@ -59,7 +51,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
{cyclesList ? (
|
{cyclesList ? (
|
||||||
<CyclesBoard
|
<CyclesBoard
|
||||||
cycles={cyclesList}
|
cycleIds={cyclesList}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -78,7 +70,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||||||
{layout === "gantt" && (
|
{layout === "gantt" && (
|
||||||
<>
|
<>
|
||||||
{cyclesList ? (
|
{cyclesList ? (
|
||||||
<CyclesListGanttChartView cycles={cyclesList} workspaceSlug={workspaceSlug} />
|
<CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-4">
|
<Loader className="space-y-4">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
// next
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useApplication, useCycle } from "hooks/store";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
|
|
||||||
interface ICycleDelete {
|
interface ICycleDelete {
|
||||||
cycle: ICycle;
|
cycle: ICycle;
|
||||||
@ -23,24 +21,25 @@ interface ICycleDelete {
|
|||||||
|
|
||||||
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||||
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
|
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
|
||||||
// store
|
|
||||||
const {
|
|
||||||
cycle: cycleStore,
|
|
||||||
trackEvent: { postHogEventTracker },
|
|
||||||
} = useMobxStore();
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// states
|
// states
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { cycleId, peekCycle } = router.query;
|
const { cycleId, peekCycle } = router.query;
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
eventTracker: { postHogEventTracker },
|
||||||
|
} = useApplication();
|
||||||
|
const { deleteCycle } = useCycle();
|
||||||
|
// toast alert
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const formSubmit = async () => {
|
const formSubmit = async () => {
|
||||||
|
if (!cycle) return;
|
||||||
|
|
||||||
setLoader(true);
|
setLoader(true);
|
||||||
if (cycle?.id)
|
|
||||||
try {
|
try {
|
||||||
await cycleStore
|
await deleteCycle(workspaceSlug, projectId, cycle.id)
|
||||||
.removeCycle(workspaceSlug, projectId, cycle?.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -67,12 +66,6 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
|||||||
message: "Something went wrong please try again later.",
|
message: "Something went wrong please try again later.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Warning!",
|
|
||||||
message: "Something went wrong please try again later.",
|
|
||||||
});
|
|
||||||
|
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { KeyedMutator } from "swr";
|
import { KeyedMutator } from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useCycle, useUser } from "hooks/store";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// components
|
// components
|
||||||
@ -16,7 +16,7 @@ import { EUserWorkspaceRoles } from "constants/workspace";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
cycles: ICycle[];
|
cycleIds: string[];
|
||||||
mutateCycles?: KeyedMutator<ICycle[]>;
|
mutateCycles?: KeyedMutator<ICycle[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ type Props = {
|
|||||||
const cycleService = new CycleService();
|
const cycleService = new CycleService();
|
||||||
|
|
||||||
export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||||
const { cycles, mutateCycles } = props;
|
const { cycleIds, mutateCycles } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -32,6 +32,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { getCycleById } = useCycle();
|
||||||
|
|
||||||
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
|
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
@ -65,18 +66,21 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
|||||||
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
|
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const blockFormat = (blocks: ICycle[]) =>
|
const blockFormat = (blocks: (ICycle | null)[]) => {
|
||||||
blocks && blocks.length > 0
|
if (!blocks) return [];
|
||||||
? blocks
|
|
||||||
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date))
|
const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date);
|
||||||
.map((block) => ({
|
|
||||||
|
const structuredBlocks = filteredBlocks.map((block) => ({
|
||||||
data: block,
|
data: block,
|
||||||
id: block.id,
|
id: block?.id ?? "",
|
||||||
sort_order: block.sort_order,
|
sort_order: block?.sort_order ?? 0,
|
||||||
start_date: new Date(block.start_date ?? ""),
|
start_date: new Date(block?.start_date ?? ""),
|
||||||
target_date: new Date(block.end_date ?? ""),
|
target_date: new Date(block?.end_date ?? ""),
|
||||||
}))
|
}));
|
||||||
: [];
|
|
||||||
|
return structuredBlocks;
|
||||||
|
};
|
||||||
|
|
||||||
const isAllowed =
|
const isAllowed =
|
||||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
@ -86,7 +90,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
|||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
title="Cycles"
|
title="Cycles"
|
||||||
loaderTitle="Cycles"
|
loaderTitle="Cycles"
|
||||||
blocks={cycles ? blockFormat(cycles) : null}
|
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
|
||||||
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
||||||
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
|
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
|
||||||
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}
|
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}
|
||||||
|
@ -3,8 +3,8 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useApplication, useCycle } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// components
|
// components
|
||||||
import { CycleForm } from "components/cycles";
|
import { CycleForm } from "components/cycles";
|
||||||
// types
|
// types
|
||||||
@ -23,21 +23,21 @@ const cycleService = new CycleService();
|
|||||||
|
|
||||||
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||||
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
|
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
|
||||||
// store
|
|
||||||
const {
|
|
||||||
cycle: cycleStore,
|
|
||||||
trackEvent: { postHogEventTracker },
|
|
||||||
} = useMobxStore();
|
|
||||||
// states
|
// states
|
||||||
const [activeProject, setActiveProject] = useState<string>(projectId);
|
const [activeProject, setActiveProject] = useState<string>(projectId);
|
||||||
// toast
|
// store hooks
|
||||||
|
const {
|
||||||
|
eventTracker: { postHogEventTracker },
|
||||||
|
} = useApplication();
|
||||||
|
const { createCycle, updateCycleDetails } = useCycle();
|
||||||
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const createCycle = async (payload: Partial<ICycle>) => {
|
const handleCreateCycle = async (payload: Partial<ICycle>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project ?? projectId.toString();
|
||||||
await cycleStore
|
await createCycle(workspaceSlug, selectedProjectId, payload)
|
||||||
.createCycle(workspaceSlug, selectedProjectId, payload)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
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;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project ?? projectId.toString();
|
||||||
await cycleStore
|
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
||||||
.patchCycle(workspaceSlug, selectedProjectId, cycleId, payload)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
if (data) await updateCycle(data.id, payload);
|
if (data) await handleUpdateCycle(data.id, payload);
|
||||||
else await createCycle(payload);
|
else await handleCreateCycle(payload);
|
||||||
handleClose();
|
handleClose();
|
||||||
} else
|
} else
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -3,11 +3,10 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useApplication, useCycle, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { SidebarProgressStats } from "components/core";
|
import { SidebarProgressStats } from "components/core";
|
||||||
@ -46,19 +45,21 @@ const cycleService = new CycleService();
|
|||||||
// TODO: refactor the whole component
|
// TODO: refactor the whole component
|
||||||
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { cycleId, handleClose } = props;
|
const { cycleId, handleClose } = props;
|
||||||
|
// states
|
||||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||||
|
// store hooks
|
||||||
const {
|
const {
|
||||||
cycle: cycleDetailsStore,
|
eventTracker: { setTrackElement },
|
||||||
trackEvent: { setTrackElement },
|
} = useApplication();
|
||||||
user: { currentProjectRole },
|
const {
|
||||||
} = useMobxStore();
|
membership: { currentProjectRole },
|
||||||
|
} = useUser();
|
||||||
|
const { getCycleById, updateCycleDetails } = useCycle();
|
||||||
|
|
||||||
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const submitChanges = (data: Partial<ICycle>) => {
|
const submitChanges = (data: Partial<ICycle>) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
|
|
||||||
cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
|
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
|
@ -2,8 +2,9 @@ import React, { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// mobx store
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { useCycle } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
CycleAppliedFiltersRoot,
|
CycleAppliedFiltersRoot,
|
||||||
@ -29,12 +30,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
cycleId: string;
|
cycleId: string;
|
||||||
};
|
};
|
||||||
|
// store hooks
|
||||||
const {
|
const {
|
||||||
cycle: cycleStore,
|
cycle: cycleStore,
|
||||||
cycleIssues: { loader, getIssues, fetchIssues },
|
cycleIssues: { loader, getIssues, fetchIssues },
|
||||||
cycleIssuesFilter: { issueFilters, fetchFilters },
|
cycleIssuesFilter: { issueFilters, fetchFilters },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
const { getCycleById } = useCycle();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
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 activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined;
|
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
|
||||||
const cycleStatus =
|
const cycleStatus =
|
||||||
cycleDetails?.start_date && cycleDetails?.end_date
|
cycleDetails?.start_date && cycleDetails?.end_date
|
||||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
|
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",
|
key: "all",
|
||||||
name: "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",
|
key: "list",
|
||||||
icon: List,
|
icon: List,
|
||||||
|
@ -10,6 +10,7 @@ import { JoinProject } from "components/auth-screens";
|
|||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
// images
|
// images
|
||||||
import emptyProject from "public/empty-state/project.svg";
|
import emptyProject from "public/empty-state/project.svg";
|
||||||
|
import { useApplication, useCycle, useModule, useProjectState, useUser } from "hooks/store";
|
||||||
|
|
||||||
interface IProjectAuthWrapper {
|
interface IProjectAuthWrapper {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -19,18 +20,22 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
const { children } = props;
|
const { children } = props;
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
|
|
||||||
project: { fetchProjectDetails, workspaceProjects },
|
project: { fetchProjectDetails, workspaceProjects },
|
||||||
projectLabel: { fetchProjectLabels },
|
projectLabel: { fetchProjectLabels },
|
||||||
projectMember: { fetchProjectMembers },
|
projectMember: { fetchProjectMembers },
|
||||||
projectState: { fetchProjectStates },
|
|
||||||
projectEstimates: { fetchProjectEstimates },
|
projectEstimates: { fetchProjectEstimates },
|
||||||
cycle: { fetchCycles },
|
|
||||||
module: { fetchModules },
|
|
||||||
projectViews: { fetchAllViews },
|
projectViews: { fetchAllViews },
|
||||||
inbox: { fetchInboxesList, isInboxEnabled },
|
inbox: { fetchInboxesList, isInboxEnabled },
|
||||||
commandPalette: { toggleCreateProjectModal },
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
const {
|
||||||
|
commandPalette: { toggleCreateProjectModal },
|
||||||
|
} = useApplication();
|
||||||
|
const {
|
||||||
|
membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
|
||||||
|
} = useUser();
|
||||||
|
const { fetchAllCycles } = useCycle();
|
||||||
|
const { fetchModules } = useModule();
|
||||||
|
const { fetchProjectStates } = useProjectState();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -68,7 +73,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
// fetching project cycles
|
// fetching project cycles
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
|
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
|
// fetching project modules
|
||||||
useSWR(
|
useSWR(
|
||||||
@ -80,7 +85,6 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
|
||||||
workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null
|
workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null
|
||||||
);
|
);
|
||||||
// TODO: fetching project pages
|
|
||||||
// fetching project inboxes if inbox is enabled
|
// fetching project inboxes if inbox is enabled
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null,
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useCycle } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
@ -19,24 +18,23 @@ import emptyCycle from "public/empty-state/cycle.svg";
|
|||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
|
||||||
const CycleDetailPage: NextPageWithLayout = () => {
|
const CycleDetailPage: NextPageWithLayout = () => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
// store hooks
|
||||||
const { cycle: cycleStore } = useMobxStore();
|
const { fetchCycleDetails } = useCycle();
|
||||||
|
|
||||||
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||||
|
|
||||||
const { error } = useSWR(
|
const { error } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null,
|
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
|
||||||
workspaceSlug && projectId && cycleId
|
workspaceSlug && projectId && cycleId
|
||||||
? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
|
||||||
setValue(`${!isSidebarCollapsed}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -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 { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Tab } from "@headlessui/react";
|
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 { useCycle, useUser } from "hooks/store";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { CyclesHeader } from "components/headers";
|
import { CyclesHeader } from "components/headers";
|
||||||
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
||||||
|
import { NewEmptyState } from "components/common/new-empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// images
|
// images
|
||||||
@ -20,64 +22,37 @@ import { NextPageWithLayout } from "types/app";
|
|||||||
// constants
|
// constants
|
||||||
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
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 ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||||
const [createModal, setCreateModal] = useState(false);
|
const [createModal, setCreateModal] = useState(false);
|
||||||
// store
|
// store hooks
|
||||||
const {
|
const {
|
||||||
cycle: cycleStore,
|
membership: { currentProjectRole },
|
||||||
user: { currentProjectRole },
|
} = useUser();
|
||||||
} = useMobxStore();
|
const { projectAllCycles } = useCycle();
|
||||||
const { projectCycles } = cycleStore;
|
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
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(
|
const handleCurrentLayout = useCallback(
|
||||||
(_layout: TCycleLayout) => {
|
(_layout: TCycleLayout) => {
|
||||||
if (projectId) {
|
setCycleLayout(_layout);
|
||||||
setLocalStorage(`cycle_layout:${projectId}`, _layout);
|
|
||||||
cycleStore.setCycleLayout(_layout);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[cycleStore, projectId]
|
[setCycleLayout]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCurrentView = useCallback(
|
const handleCurrentView = useCallback(
|
||||||
(_view: TCycleView) => {
|
(_view: TCycleView) => {
|
||||||
if (projectId) {
|
setCycleTab(_view);
|
||||||
setLocalStorage(`cycle_view:${projectId}`, _view);
|
if (_view === "draft") handleCurrentLayout("list");
|
||||||
cycleStore.setCycleView(_view);
|
|
||||||
if (_view === "draft" && cycleStore.cycleLayout === "gantt") {
|
|
||||||
handleCurrentLayout("list");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[cycleStore, projectId, handleCurrentLayout]
|
[handleCurrentLayout, setCycleTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const totalCycles = projectAllCycles?.length ?? 0;
|
||||||
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 isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
@ -117,11 +92,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
<Tab.Group
|
<Tab.Group
|
||||||
as="div"
|
as="div"
|
||||||
className="flex h-full flex-col overflow-hidden"
|
className="flex h-full flex-col overflow-hidden"
|
||||||
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)}
|
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||||
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)}
|
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||||
onChange={(i) => {
|
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
|
||||||
handleCurrentView(CYCLE_TAB_LIST[i].key as TCycleView);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<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">
|
<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">
|
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||||
@ -138,26 +111,24 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
{cycleStore?.cycleView != "active" && (
|
{cycleTab !== "active" && (
|
||||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||||
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
||||||
if (layout.key === "gantt" && cycleStore?.cycleView === "draft") return null;
|
if (layout.key === "gantt" && cycleTab === "draft") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||||
cycleStore?.cycleLayout == layout.key
|
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||||
? "bg-custom-background-100 shadow-custom-shadow-2xs"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
||||||
>
|
>
|
||||||
<layout.icon
|
<layout.icon
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
className={`h-3.5 w-3.5 ${
|
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>
|
</button>
|
||||||
@ -170,10 +141,10 @@ 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 && (
|
{cycleTab && cycleLayout && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"all"}
|
filter="all"
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
peekCycle={peekCycle?.toString()}
|
peekCycle={peekCycle?.toString()}
|
||||||
@ -186,9 +157,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
</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 && (
|
{cycleTab && cycleLayout && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"upcoming"}
|
filter="upcoming"
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
@ -198,9 +169,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
</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 && (
|
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"completed"}
|
filter="completed"
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
@ -210,9 +181,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
</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 && (
|
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"draft"}
|
filter="draft"
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
|
@ -11,7 +11,7 @@ export class CycleService extends APIService {
|
|||||||
super(API_BASE_URL);
|
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)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -22,8 +22,8 @@ export class CycleService extends APIService {
|
|||||||
async getCyclesWithParams(
|
async getCyclesWithParams(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
cycleType?: "current"
|
||||||
): Promise<ICycle[]> {
|
): Promise<Record<string, ICycle>> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, {
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, {
|
||||||
params: {
|
params: {
|
||||||
cycle_view: cycleType,
|
cycle_view: cycleType,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
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
|
// types
|
||||||
import { ICycle, TCycleView, CycleDateCheckData } from "types";
|
import { ICycle, CycleDateCheckData } from "types";
|
||||||
// mobx
|
// mobx
|
||||||
import { RootStore } from "store/root.store";
|
import { RootStore } from "store/root.store";
|
||||||
// services
|
// services
|
||||||
@ -10,40 +11,30 @@ import { IssueService } from "services/issue";
|
|||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
|
|
||||||
export interface ICycleStore {
|
export interface ICycleStore {
|
||||||
|
// states
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
error: any | null;
|
error: any | null;
|
||||||
|
// observables
|
||||||
cycleView: TCycleView;
|
|
||||||
|
|
||||||
cycleId: string | null;
|
|
||||||
cycleMap: {
|
cycleMap: {
|
||||||
[projectId: string]: {
|
|
||||||
[cycleId: string]: ICycle;
|
[cycleId: string]: ICycle;
|
||||||
};
|
};
|
||||||
|
activeCycleMap: {
|
||||||
|
[cycleId: string]: ICycle;
|
||||||
};
|
};
|
||||||
cycles: {
|
|
||||||
[projectId: string]: {
|
|
||||||
[filterType: string]: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
getCycleById: (cycleId: string) => ICycle | null;
|
projectAllCycles: string[] | null;
|
||||||
projectCycles: string[] | null;
|
|
||||||
projectCompletedCycles: string[] | null;
|
projectCompletedCycles: string[] | null;
|
||||||
projectUpcomingCycles: string[] | null;
|
projectUpcomingCycles: string[] | null;
|
||||||
projectDraftCycles: string[] | null;
|
projectDraftCycles: string[] | null;
|
||||||
|
projectActiveCycle: string | null;
|
||||||
|
// computed actions
|
||||||
|
getCycleById: (cycleId: string) => ICycle | null;
|
||||||
|
getActiveCycleById: (cycleId: string) => ICycle | null;
|
||||||
// actions
|
// actions
|
||||||
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
|
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
|
||||||
|
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise<Record<string, ICycle>>;
|
||||||
fetchCycles: (
|
fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise<Record<string, ICycle>>;
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
|
||||||
) => Promise<void>;
|
|
||||||
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
||||||
|
|
||||||
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
|
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
|
||||||
updateCycleDetails: (
|
updateCycleDetails: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -52,29 +43,19 @@ export interface ICycleStore {
|
|||||||
data: Partial<ICycle>
|
data: Partial<ICycle>
|
||||||
) => Promise<ICycle>;
|
) => Promise<ICycle>;
|
||||||
deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
||||||
|
|
||||||
addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
|
addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
|
||||||
removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CycleStore implements ICycleStore {
|
export class CycleStore implements ICycleStore {
|
||||||
|
// states
|
||||||
loader: boolean = false;
|
loader: boolean = false;
|
||||||
error: any | null = null;
|
error: any | null = null;
|
||||||
|
// observables
|
||||||
cycleView: TCycleView = "all";
|
|
||||||
|
|
||||||
cycleId: string | null = null;
|
|
||||||
cycleMap: {
|
cycleMap: {
|
||||||
[projectId: string]: {
|
|
||||||
[cycleId: string]: ICycle;
|
[cycleId: string]: ICycle;
|
||||||
};
|
|
||||||
} = {};
|
} = {};
|
||||||
cycles: {
|
activeCycleMap: { [cycleId: string]: ICycle } = {};
|
||||||
[projectId: string]: {
|
|
||||||
[filterType: string]: string[];
|
|
||||||
};
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
// services
|
// services
|
||||||
@ -84,29 +65,28 @@ export class CycleStore implements ICycleStore {
|
|||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
loader: observable,
|
// states
|
||||||
|
loader: observable.ref,
|
||||||
error: observable.ref,
|
error: observable.ref,
|
||||||
|
// observables
|
||||||
cycleId: observable.ref,
|
|
||||||
cycleMap: observable,
|
cycleMap: observable,
|
||||||
cycles: observable,
|
activeCycleMap: observable,
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
projectCycles: computed,
|
projectAllCycles: computed,
|
||||||
projectCompletedCycles: computed,
|
projectCompletedCycles: computed,
|
||||||
projectUpcomingCycles: computed,
|
projectUpcomingCycles: computed,
|
||||||
projectDraftCycles: computed,
|
projectDraftCycles: computed,
|
||||||
|
projectActiveCycle: computed,
|
||||||
// actions
|
// computed actions
|
||||||
getCycleById: action,
|
getCycleById: action,
|
||||||
|
getActiveCycleById: action,
|
||||||
fetchCycles: action,
|
// actions
|
||||||
|
fetchAllCycles: action,
|
||||||
|
fetchActiveCycle: action,
|
||||||
fetchCycleDetails: action,
|
fetchCycleDetails: action,
|
||||||
|
|
||||||
createCycle: action,
|
createCycle: action,
|
||||||
updateCycleDetails: action,
|
updateCycleDetails: action,
|
||||||
deleteCycle: action,
|
deleteCycle: action,
|
||||||
|
|
||||||
addCycleToFavorites: action,
|
addCycleToFavorites: action,
|
||||||
removeCycleFromFavorites: action,
|
removeCycleFromFavorites: action,
|
||||||
});
|
});
|
||||||
@ -118,46 +98,86 @@ export class CycleStore implements ICycleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
get projectCycles() {
|
get projectAllCycles() {
|
||||||
const projectId = this.rootStore.app.router.projectId;
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
|
||||||
if (!projectId) return null;
|
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() {
|
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() {
|
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() {
|
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;
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
|
||||||
if (!projectId) return null;
|
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) => {
|
validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
|
const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
|
||||||
@ -168,27 +188,52 @@ export class CycleStore implements ICycleStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCycles = async (
|
fetchAllCycles = async (workspaceSlug: string, projectId: string) => {
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params);
|
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.cycleMap, [projectId], cyclesResponse);
|
Object.values(cyclesResponse).forEach((cycle) => {
|
||||||
set(this.cycles, [projectId, params], Object.keys(cyclesResponse));
|
set(this.cycleMap, [cycle.id], cycle);
|
||||||
|
});
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return cyclesResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch project cycles in project store", error);
|
console.error("Failed to fetch project cycles in project store", error);
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = error;
|
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);
|
const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId);
|
||||||
|
|
||||||
runInAction(() => {
|
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;
|
return response;
|
||||||
@ -212,12 +258,10 @@ export class CycleStore implements ICycleStore {
|
|||||||
const response = await this.cycleService.createCycle(workspaceSlug, projectId, data);
|
const response = await this.cycleService.createCycle(workspaceSlug, projectId, data);
|
||||||
|
|
||||||
runInAction(() => {
|
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;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to create cycle from cycle store");
|
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>) => {
|
updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial<ICycle>) => {
|
||||||
try {
|
try {
|
||||||
const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data);
|
const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data);
|
||||||
|
|
||||||
const currentCycle = this.cycleMap[projectId][cycleId];
|
|
||||||
|
|
||||||
runInAction(() => {
|
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;
|
return response;
|
||||||
this.fetchCycles(workspaceSlug, projectId, _currentView);
|
|
||||||
|
|
||||||
return _response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to patch cycle from cycle store");
|
console.log("Failed to patch cycle from cycle store");
|
||||||
throw error;
|
throw error;
|
||||||
@ -246,32 +286,36 @@ export class CycleStore implements ICycleStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||||
try {
|
const originalCycle = this.cycleMap[cycleId];
|
||||||
if (!this.cycleMap?.[projectId]?.[cycleId]) return;
|
const originalActiveCycle = this.activeCycleMap[cycleId];
|
||||||
|
|
||||||
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
delete this.cycleMap[projectId][cycleId];
|
omit(this.cycleMap, [cycleId]);
|
||||||
|
omit(this.activeCycleMap, [cycleId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId);
|
await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId);
|
||||||
|
|
||||||
return _response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to delete cycle from cycle store");
|
console.log("Failed to delete cycle from cycle store");
|
||||||
|
|
||||||
const _currentView = this.cycleView === "active" ? "current" : this.cycleView;
|
runInAction(() => {
|
||||||
this.fetchCycles(workspaceSlug, projectId, _currentView);
|
set(this.cycleMap, [cycleId], originalCycle);
|
||||||
|
set(this.activeCycleMap, [cycleId], originalActiveCycle);
|
||||||
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||||
try {
|
try {
|
||||||
const currentCycle = this.cycleMap[projectId][cycleId];
|
const currentCycle = this.getCycleById(cycleId);
|
||||||
if (currentCycle.is_favorite) return;
|
const currentActiveCycle = this.getActiveCycleById(cycleId);
|
||||||
|
|
||||||
runInAction(() => {
|
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.
|
// updating through api.
|
||||||
@ -279,10 +323,12 @@ export class CycleStore implements ICycleStore {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} 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(() => {
|
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;
|
throw error;
|
||||||
@ -291,22 +337,24 @@ export class CycleStore implements ICycleStore {
|
|||||||
|
|
||||||
removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||||
try {
|
try {
|
||||||
const currentCycle = this.cycleMap[projectId][cycleId];
|
const currentCycle = this.getCycleById(cycleId);
|
||||||
|
const currentActiveCycle = this.getActiveCycleById(cycleId);
|
||||||
if (!currentCycle.is_favorite) return;
|
|
||||||
|
|
||||||
runInAction(() => {
|
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);
|
const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to remove cycle from favorites - Cycle Store", error);
|
const currentCycle = this.getCycleById(cycleId);
|
||||||
|
const currentActiveCycle = this.getActiveCycleById(cycleId);
|
||||||
|
|
||||||
runInAction(() => {
|
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;
|
throw error;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
||||||
import set from "lodash/set";
|
import { set } from "lodash";
|
||||||
// services
|
// services
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
import { ModuleService } from "services/module.service";
|
import { ModuleService } from "services/module.service";
|
||||||
|
Loading…
Reference in New Issue
Block a user