dev: workspace active cycles (#3378)

* chore: workspace active cycles

* fix: active cycles tab implementation

* chore: added distribution graph for active cycles

* chore: removed distribution graph and issues

* Revert "chore: removed issues"

This reverts commit 7d977ac8b0.

* chore: workspace active cycles implementation

* chore: code refactor

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2024-01-16 19:54:32 +05:30 committed by GitHub
parent 9e8885df5f
commit 8ee5ba96ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 692 additions and 6 deletions

View File

@ -8,10 +8,16 @@ from plane.app.views import (
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
ActiveCycleEndpoint
)
urlpatterns = [
path(
"workspaces/<str:slug>/active-cycles/",
ActiveCycleEndpoint.as_view(),
name="workspace-active-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view(

View File

@ -62,6 +62,7 @@ from .cycle import (
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
ActiveCycleEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .issue import (

View File

@ -39,6 +39,7 @@ from plane.app.serializers import (
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
WorkspaceUserPermission
)
from plane.db.models import (
User,
@ -909,3 +910,235 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
)
serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_200_OK)
class ActiveCycleEndpoint(BaseAPIView):
permission_classes = [
WorkspaceUserPermission,
]
def get(self, request, slug):
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
active_cycles = (
Cycle.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=self.request.user,
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT"),
),
default=Value("DRAFT"),
output_field=CharField(),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only("name", "color", "id").distinct(),
)
)
.order_by("-created_at")
)
cycles = CycleSerializer(active_cycles, many=True).data
for cycle in cycles:
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle["id"],
project_id=cycle["project"],
workspace__slug=slug,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle["id"],
project_id=cycle["project"],
workspace__slug=slug,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
cycle["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
if cycle["start_date"] and cycle["end_date"]:
cycle["distribution"][
"completion_chart"
] = burndown_plot(
queryset=active_cycles.get(pk=cycle["id"]),
slug=slug,
project_id=cycle["project"],
cycle_id=cycle["id"],
)
return Response(cycles, status=status.HTTP_200_OK)

View File

@ -1,4 +1,11 @@
import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types";
import type {
IUser,
TIssue,
IProjectLite,
IWorkspaceLite,
IIssueFilterOptions,
IUserLite,
} from "@plane/types";
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
@ -40,6 +47,7 @@ export interface ICycle {
};
workspace: string;
workspace_detail: IWorkspaceLite;
issues?: TIssue[];
}
export type TAssigneesDistribution = {
@ -80,9 +88,13 @@ export interface CycleIssueResponse {
sub_issues_count: number;
}
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
export type SelectCycleType =
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
| undefined;
export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null;
export type SelectIssue =
| (TIssue & { actionType: "edit" | "delete" | "create" })
| null;
export type CycleDateCheckData = {
start_date: string;

View File

@ -1,5 +1,11 @@
import { EUserProjectRoles } from "constants/project";
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
import type {
IUser,
IUserLite,
IWorkspace,
IWorkspaceLite,
TStateGroups,
} from ".";
export interface IProject {
archive_in: number;
@ -52,6 +58,11 @@ export interface IProjectLite {
id: string;
name: string;
identifier: string;
emoji: string | null;
icon_prop: {
name: string;
color: string;
} | null;
}
type ProjectPreferences = {

View File

@ -0,0 +1,254 @@
import { FC, MouseEvent, useCallback } from "react";
import Link from "next/link";
// ui
import {
AvatarGroup,
Tooltip,
LinearProgressIndicator,
ContrastIcon,
RunningIcon,
LayersIcon,
StateGroupIcon,
Avatar,
} from "@plane/ui";
// components
import { SingleProgressStats } from "components/core";
import { ActiveCycleProgressStats } from "./active-cycle-stats";
// hooks
import { useCycle } from "hooks/store";
import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// icons
import { ArrowRight, CalendarDays, Star, Target } from "lucide-react";
// types
import { ICycle, TCycleLayout, TCycleView } from "@plane/types";
// helpers
import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper";
import { truncateText } from "helpers/string.helper";
// constants
import { STATE_GROUPS_DETAILS } from "constants/cycle";
export type ActiveCycleInfoProps = {
cycle: ICycle;
workspaceSlug: string;
projectId: string;
};
export const ActiveCycleInfo: FC<ActiveCycleInfoProps> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
// store
const { addCycleToFavorites, removeCycleFromFavorites } = useCycle();
// local storage
const { setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
// toast alert
const { setToastAlert } = useToast();
const groupedIssues: any = {
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
started: cycle.started_issues,
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
};
const progressIndicatorData = STATE_GROUPS_DETAILS.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
color: group.color,
}));
const handleCurrentLayout = useCallback(
(_layout: TCycleLayout) => {
setCycleLayout(_layout);
},
[setCycleLayout]
);
const handleCurrentView = useCallback(
(_view: TCycleView) => {
setCycleTab(_view);
if (_view === "draft") handleCurrentLayout("list");
},
[handleCurrentLayout, setCycleTab]
);
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
return (
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:grid-cols-3 lg:divide-x lg:divide-y-0">
<div className="flex flex-col text-xs">
<div className="h-full w-full">
<div className="flex h-60 flex-col justify-between gap-5 rounded-b-[10px] p-4">
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-1">
<span className="h-5 w-5">
<ContrastIcon className="h-5 w-5" color="#09A953" />
</span>
<Tooltip tooltipContent={cycle.name} position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 70)}</h3>
</Tooltip>
</span>
<span className="flex items-center gap-1 capitalize">
<span className="rounded-full px-1.5 py-0.5 bg-green-600/5 text-green-600">
<span className="flex gap-1 whitespace-nowrap">
<RunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
</span>
</span>
{cycle.is_favorite ? (
<button
onClick={(e) => {
handleRemoveFromFavorites(e);
}}
>
<Star className="h-4 w-4 fill-orange-400 text-orange-400" />
</button>
) : (
<button
onClick={(e) => {
handleAddToFavorites(e);
}}
>
<Star className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button>
)}
</span>
</div>
<div className="flex items-center justify-start gap-5 text-custom-text-200">
<div className="flex items-start gap-1">
<CalendarDays className="h-4 w-4" />
{cycle?.start_date && <span>{renderFormattedDate(cycle?.start_date)}</span>}
</div>
<ArrowRight className="h-4 w-4 text-custom-text-200" />
<div className="flex items-start gap-1">
<Target className="h-4 w-4" />
{cycle?.end_date && <span>{renderFormattedDate(cycle?.end_date)}</span>}
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
{cycle.assignees.length > 0 && (
<div className="flex items-center gap-1 text-custom-text-200">
<AvatarGroup>
{cycle.assignees.map((assignee: any) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))}
</AvatarGroup>
</div>
)}
</div>
<div className="flex items-center gap-4 text-custom-text-200">
<div className="flex gap-2">
<LayersIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues
</div>
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
{cycle.completed_issues} issues
</div>
</div>
<div className="flex item-center gap-2">
<Link
href={`/${workspaceSlug}/projects/${projectId}/cycles`}
onClick={() => {
handleCurrentView("active");
}}
>
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
View Cycle
</span>
</Link>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
View Cycle Issues
</span>
</Link>
</div>
</div>
</div>
</div>
<div className="col-span-2 grid grid-cols-1 divide-y border-custom-border-200 md:grid-cols-2 md:divide-x md:divide-y-0">
<div className="flex h-60 flex-col border-custom-border-200">
<div className="flex h-full w-full flex-col p-4 text-custom-text-200">
<div className="flex w-full items-center gap-2 py-1">
<span>Progress</span>
<LinearProgressIndicator data={progressIndicatorData} />
</div>
<div className="mt-2 flex flex-col items-center gap-1">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: STATE_GROUPS_DETAILS[index].color,
}}
/>
<span className="text-xs capitalize">{group}</span>
</div>
}
completed={groupedIssues[group]}
total={cycle.total_issues}
/>
))}
</div>
</div>
</div>
<div className="h-60 overflow-y-scroll border-custom-border-200">
<ActiveCycleProgressStats cycle={cycle} />
</div>
</div>
</div>
</div>
);
};

View File

@ -15,3 +15,4 @@ export * from "./cycles-board-card";
export * from "./delete-modal";
export * from "./cycle-peek-overview";
export * from "./cycles-list-item";
export * from "./active-cycle-info";

View File

@ -20,3 +20,4 @@ export * from "./project-archived-issue-details";
export * from "./project-archived-issues";
export * from "./project-issue-details";
export * from "./user-profile";
export * from "./workspace-active-cycle";

View File

@ -0,0 +1,37 @@
import { observer } from "mobx-react-lite";
import { Search, SendToBack } from "lucide-react";
// hooks
import { useWorkspace } from "hooks/store";
// ui
import { Breadcrumbs } from "@plane/ui";
export const WorkspaceActiveCycleHeader = observer(() => {
// store hooks
const { workspaceActiveCyclesSearchQuery, setWorkspaceActiveCyclesSearchQuery } = useWorkspace();
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
icon={<SendToBack className="h-4 w-4 text-custom-text-300" />}
label="Active Cycles"
/>
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex w-full items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400">
<Search className="h-3.5 w-3.5" />
<input
className="w-full min-w-[234px] border-none bg-transparent text-sm focus:outline-none"
value={workspaceActiveCyclesSearchQuery}
onChange={(e) => setWorkspaceActiveCyclesSearchQuery(e.target.value)}
placeholder="Search"
/>
</div>
</div>
</div>
);
});

View File

@ -9,7 +9,7 @@ type Props = {
export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 }) => (
<>
{labelDetails.length > 0 ? (
{labelDetails?.length > 0 ? (
labelDetails.length <= maxRender ? (
<>
{labelDetails.map((label) => (

View File

@ -13,3 +13,4 @@ export * from "./send-workspace-invitation-modal";
export * from "./sidebar-dropdown";
export * from "./sidebar-menu";
export * from "./sidebar-quick-action";
export * from "./workspace-active-cycles-list";

View File

@ -2,7 +2,7 @@ import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react";
import { BarChart2, Briefcase, CheckCircle, LayoutGrid, SendToBack } from "lucide-react";
// hooks
import { useApplication, useUser } from "hooks/store";
// components
@ -33,6 +33,11 @@ const workspaceLinks = (workspaceSlug: string) => [
name: "All Issues",
href: `/${workspaceSlug}/workspace-views/all-issues`,
},
{
Icon: SendToBack,
name: "Active Cycles",
href: `/${workspaceSlug}/active-cycles`,
},
];
export const WorkspaceSidebarMenu = observer(() => {

View File

@ -0,0 +1,57 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import useSWR from "swr";
// components
import { ActiveCycleInfo } from "components/cycles";
// services
import { CycleService } from "services/cycle.service";
const cycleService = new CycleService();
// hooks
import { useWorkspace } from "hooks/store";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
export const WorkspaceActiveCyclesList = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// fetching active cycles in workspace
const { data } = useSWR("WORKSPACE_ACTIVE_CYCLES", () => cycleService.workspaceActiveCycles(workspaceSlug as string));
// store
const { workspaceActiveCyclesSearchQuery } = useWorkspace();
// filter cycles based on search query
const filteredCycles = data?.filter(
(cycle) =>
cycle.project_detail.name.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) ||
cycle.project_detail.identifier?.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) ||
cycle.name.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase())
);
return (
<div>
{workspaceSlug &&
filteredCycles &&
filteredCycles.map((cycle) => (
<div key={cycle.id} className="px-5 py-7">
<div className="flex items-center gap-1.5 px-3 py-1.5">
{cycle.project_detail.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(cycle.project_detail.emoji)}
</span>
) : cycle.project_detail.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(cycle.project_detail.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{cycle.project_detail?.name.charAt(0)}
</span>
)}
<h2 className="text-xl font-semibold">{cycle.project_detail.name}</h2>
</div>
<ActiveCycleInfo workspaceSlug={workspaceSlug?.toString()} projectId={cycle.project} cycle={cycle} />
</div>
))}
</div>
);
});

View File

@ -86,3 +86,32 @@ export const CYCLE_STATUS: {
bgColor: "bg-custom-background-90",
},
];
export const STATE_GROUPS_DETAILS = [
{
key: "backlog_issues",
title: "Backlog",
color: "#dee2e6",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#26b5ce",
},
{
key: "started_issues",
title: "Started",
color: "#f7ae59",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#d687ff",
},
{
key: "completed_issues",
title: "Completed",
color: "#09a953",
},
];

View File

@ -0,0 +1,16 @@
import { ReactElement } from "react";
// components
import { WorkspaceActiveCyclesList } from "components/workspace";
import { WorkspaceActiveCycleHeader } from "components/headers";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
import { NextPageWithLayout } from "lib/types";
const WorkspaceActiveCyclesPage: NextPageWithLayout = () => <WorkspaceActiveCyclesList />;
WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;
};
export default WorkspaceActiveCyclesPage;

View File

@ -10,6 +10,14 @@ export class CycleService extends APIService {
super(API_BASE_URL);
}
async workspaceActiveCycles(workspaceSlug: string): Promise<ICycle[]> {
return this.get(`/api/workspaces/${workspaceSlug}/active-cycles/`)
.then((res) => res?.data)
.catch((err) => {
throw err?.response?.data;
});
}
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<ICycle> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
.then((response) => response?.data)

View File

@ -12,9 +12,12 @@ import { ApiTokenStore, IApiTokenStore } from "./api-token.store";
export interface IWorkspaceRootStore {
// observables
workspaces: Record<string, IWorkspace>;
workspaceActiveCyclesSearchQuery: string;
// computed
currentWorkspace: IWorkspace | null;
workspacesCreatedByCurrentUser: IWorkspace[] | null;
// actions
setWorkspaceActiveCyclesSearchQuery: (query: string) => void;
// computed actions
getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null;
getWorkspaceById: (workspaceId: string) => IWorkspace | null;
@ -31,6 +34,7 @@ export interface IWorkspaceRootStore {
export class WorkspaceRootStore implements IWorkspaceRootStore {
// observables
workspaceActiveCyclesSearchQuery: string = "";
workspaces: Record<string, IWorkspace> = {};
// services
workspaceService;
@ -45,6 +49,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
makeObservable(this, {
// observables
workspaces: observable,
workspaceActiveCyclesSearchQuery: observable.ref,
// computed
currentWorkspace: computed,
workspacesCreatedByCurrentUser: computed,
@ -52,6 +57,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
getWorkspaceBySlug: action,
getWorkspaceById: action,
// actions
setWorkspaceActiveCyclesSearchQuery: action,
fetchWorkspaces: action,
createWorkspace: action,
updateWorkspace: action,
@ -102,6 +108,14 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
*/
getWorkspaceById = (workspaceId: string) => this.workspaces?.[workspaceId] || null; // TODO: use undefined instead of null
/**
* Sets search query
* @param query
*/
setWorkspaceActiveCyclesSearchQuery = (query: string) => {
this.workspaceActiveCyclesSearchQuery = query;
};
/**
* fetch user workspaces from API
*/