From 8ee5ba96ce89bfd9a3c46ffddc1bce328b32b24b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 16 Jan 2024 19:54:32 +0530 Subject: [PATCH] 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 7d977ac8b0830e0f1ddcafcb70177238991eb8df. * chore: workspace active cycles implementation * chore: code refactor --------- Co-authored-by: NarayanBavisetti Co-authored-by: sriram veeraghanta --- apiserver/plane/app/urls/cycle.py | 6 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/cycle.py | 233 ++++++++++++++++ packages/types/src/cycles.d.ts | 18 +- packages/types/src/projects.d.ts | 13 +- web/components/cycles/active-cycle-info.tsx | 254 ++++++++++++++++++ web/components/cycles/index.ts | 1 + web/components/headers/index.ts | 1 + .../headers/workspace-active-cycle.tsx | 37 +++ web/components/issues/label.tsx | 2 +- web/components/workspace/index.ts | 1 + web/components/workspace/sidebar-menu.tsx | 7 +- .../workspace-active-cycles-list.tsx | 57 ++++ web/constants/cycle.ts | 29 ++ web/pages/[workspaceSlug]/active-cycles.tsx | 16 ++ web/services/cycle.service.ts | 8 + web/store/workspace/index.ts | 14 + 17 files changed, 692 insertions(+), 6 deletions(-) create mode 100644 web/components/cycles/active-cycle-info.tsx create mode 100644 web/components/headers/workspace-active-cycle.tsx create mode 100644 web/components/workspace/workspace-active-cycles-list.tsx create mode 100644 web/pages/[workspaceSlug]/active-cycles.tsx diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py index 740b0ab43..0d57e77f7 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -8,10 +8,16 @@ from plane.app.views import ( CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, + ActiveCycleEndpoint ) urlpatterns = [ + path( + "workspaces//active-cycles/", + ActiveCycleEndpoint.as_view(), + name="workspace-active-cycle", + ), path( "workspaces//projects//cycles/", CycleViewSet.as_view( diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index dccf2bb79..351ed3905 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -62,6 +62,7 @@ from .cycle import ( CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, + ActiveCycleEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue import ( diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 2d459d15b..4db0ec565 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -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) diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index 6723b3946..92ee18a42 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -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; diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a412180b8..9c963258b 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -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 = { diff --git a/web/components/cycles/active-cycle-info.tsx b/web/components/cycles/active-cycle-info.tsx new file mode 100644 index 000000000..6c64f7c6b --- /dev/null +++ b/web/components/cycles/active-cycle-info.tsx @@ -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 = (props) => { + const { cycle, workspaceSlug, projectId } = props; + + // store + const { addCycleToFavorites, removeCycleFromFavorites } = useCycle(); + // local storage + const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); + const { setValue: setCycleLayout } = useLocalStorage("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) => { + 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) => { + 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 ( +
+
+
+
+
+
+ + + + + +

{truncateText(cycle.name, 70)}

+
+
+ + + + + {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left + + + {cycle.is_favorite ? ( + + ) : ( + + )} + +
+ +
+
+ + {cycle?.start_date && {renderFormattedDate(cycle?.start_date)}} +
+ +
+ + {cycle?.end_date && {renderFormattedDate(cycle?.end_date)}} +
+
+ +
+
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.display_name} + ) : ( + + {cycle.owned_by.display_name.charAt(0)} + + )} + {cycle.owned_by.display_name} +
+ + {cycle.assignees.length > 0 && ( +
+ + {cycle.assignees.map((assignee: any) => ( + + ))} + +
+ )} +
+ +
+
+ + {cycle.total_issues} issues +
+
+ + {cycle.completed_issues} issues +
+
+
+ { + handleCurrentView("active"); + }} + > + + View Cycle + + + + + + View Cycle Issues + + +
+
+
+
+
+
+
+
+ Progress + +
+
+ {Object.keys(groupedIssues).map((group, index) => ( + + + {group} +
+ } + completed={groupedIssues[group]} + total={cycle.total_issues} + /> + ))} +
+
+
+
+ +
+
+
+ + ); +}; diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index db5e9de9e..975a03188 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -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"; diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index 298658a2a..46ce8c066 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -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"; diff --git a/web/components/headers/workspace-active-cycle.tsx b/web/components/headers/workspace-active-cycle.tsx new file mode 100644 index 000000000..f5277b354 --- /dev/null +++ b/web/components/headers/workspace-active-cycle.tsx @@ -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 ( +
+
+
+ + } + label="Active Cycles" + /> + +
+
+
+
+ + setWorkspaceActiveCyclesSearchQuery(e.target.value)} + placeholder="Search" + /> +
+
+
+ ); +}); diff --git a/web/components/issues/label.tsx b/web/components/issues/label.tsx index c66ded153..1361ff7d1 100644 --- a/web/components/issues/label.tsx +++ b/web/components/issues/label.tsx @@ -9,7 +9,7 @@ type Props = { export const ViewIssueLabel: React.FC = ({ labelDetails, maxRender = 1 }) => ( <> - {labelDetails.length > 0 ? ( + {labelDetails?.length > 0 ? ( labelDetails.length <= maxRender ? ( <> {labelDetails.map((label) => ( diff --git a/web/components/workspace/index.ts b/web/components/workspace/index.ts index 73f191dc4..dc7211296 100644 --- a/web/components/workspace/index.ts +++ b/web/components/workspace/index.ts @@ -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"; diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 98f1f880d..af78fd408 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -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(() => { diff --git a/web/components/workspace/workspace-active-cycles-list.tsx b/web/components/workspace/workspace-active-cycles-list.tsx new file mode 100644 index 000000000..85740f6c3 --- /dev/null +++ b/web/components/workspace/workspace-active-cycles-list.tsx @@ -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 ( +
+ {workspaceSlug && + filteredCycles && + filteredCycles.map((cycle) => ( +
+
+ {cycle.project_detail.emoji ? ( + + {renderEmoji(cycle.project_detail.emoji)} + + ) : cycle.project_detail.icon_prop ? ( +
+ {renderEmoji(cycle.project_detail.icon_prop)} +
+ ) : ( + + {cycle.project_detail?.name.charAt(0)} + + )} +

{cycle.project_detail.name}

+
+ +
+ ))} +
+ ); +}); diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index e2b7c4df3..1b1453503 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -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", + }, +]; diff --git a/web/pages/[workspaceSlug]/active-cycles.tsx b/web/pages/[workspaceSlug]/active-cycles.tsx new file mode 100644 index 000000000..61d57e2e6 --- /dev/null +++ b/web/pages/[workspaceSlug]/active-cycles.tsx @@ -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 = () => ; + +WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) { + return }>{page}; +}; + +export default WorkspaceActiveCyclesPage; diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 6b6d17231..7c22f34a6 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -10,6 +10,14 @@ export class CycleService extends APIService { super(API_BASE_URL); } + async workspaceActiveCycles(workspaceSlug: string): Promise { + 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 { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) .then((response) => response?.data) diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 4020aaef7..d3d7b58ac 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -12,9 +12,12 @@ import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; export interface IWorkspaceRootStore { // observables workspaces: Record; + 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 = {}; // 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 */