diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api
index c5113e059..34a50334a 100644
--- a/apiserver/Dockerfile.api
+++ b/apiserver/Dockerfile.api
@@ -48,7 +48,7 @@ USER root
RUN apk --no-cache add "bash~=5.2"
COPY ./bin ./bin/
-RUN mkdir /code/plane/logs
+RUN mkdir -p /code/plane/logs
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code
RUN chown -R captain:plane /code
diff --git a/apiserver/Dockerfile.dev b/apiserver/Dockerfile.dev
index bd6684fd5..06f15231c 100644
--- a/apiserver/Dockerfile.dev
+++ b/apiserver/Dockerfile.dev
@@ -35,6 +35,7 @@ RUN addgroup -S plane && \
COPY . .
+RUN mkdir -p /code/plane/logs
RUN chown -R captain.plane /code
RUN chmod -R +x /code/bin
RUN chmod -R 777 /code
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index bb2796bf6..637d713c3 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -481,7 +481,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
.distinct()
)
- def list(self, request, slug, project_id):
+ def get(self, request, slug, project_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py
index 460722f99..643221dca 100644
--- a/apiserver/plane/api/views/module.py
+++ b/apiserver/plane/api/views/module.py
@@ -67,6 +67,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
),
)
.annotate(
@@ -77,6 +78,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -87,6 +89,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -97,6 +100,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -107,6 +111,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -117,6 +122,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
@@ -486,6 +492,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
),
)
.annotate(
@@ -496,6 +503,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -506,6 +514,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -516,6 +525,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -526,6 +536,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -536,12 +547,13 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
)
- def list(self, request, slug, project_id):
+ def get(self, request, slug, project_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py
index b70db4c11..6809efbe6 100644
--- a/apiserver/plane/app/views/cycle/base.py
+++ b/apiserver/plane/app/views/cycle/base.py
@@ -107,6 +107,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -117,6 +118,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -127,6 +129,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -137,6 +140,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -147,6 +151,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -175,6 +180,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
+ )
+ & Q(
+ issue_cycle__issue__assignees__member_project__is_active=True
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -706,10 +714,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
+ return (
+ Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(archived_at__isnull=False)
.filter(
@@ -811,6 +817,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
+ )
+ & Q(
+ issue_cycle__issue__assignees__member_project__is_active=True
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -820,7 +829,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.distinct()
)
- def list(self, request, slug, project_id):
+ def get(self, request, slug, project_id):
queryset = (
self.get_queryset()
.annotate(
@@ -858,6 +867,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
"backlog_issues",
"assignee_ids",
"status",
+ "archived_at",
)
).order_by("-is_favorite", "-created_at")
return Response(queryset, status=status.HTTP_200_OK)
diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py
index d2a7795da..2a5505dd0 100644
--- a/apiserver/plane/app/views/cycle/issue.py
+++ b/apiserver/plane/app/views/cycle/issue.py
@@ -143,7 +143,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py
index 508f81f21..33b3cf9d5 100644
--- a/apiserver/plane/app/views/dashboard/base.py
+++ b/apiserver/plane/app/views/dashboard/base.py
@@ -149,7 +149,8 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -303,7 +304,8 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py
index fb3b9227f..710aa10a2 100644
--- a/apiserver/plane/app/views/inbox/base.py
+++ b/apiserver/plane/app/views/inbox/base.py
@@ -146,7 +146,8 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py
index 540715a24..d9274ae4f 100644
--- a/apiserver/plane/app/views/issue/archive.py
+++ b/apiserver/plane/app/views/issue/archive.py
@@ -105,7 +105,8 @@ class IssueArchiveViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py
index f1b4c7627..a27f52c74 100644
--- a/apiserver/plane/app/views/issue/base.py
+++ b/apiserver/plane/app/views/issue/base.py
@@ -52,6 +52,7 @@ from plane.db.models import (
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
+
class IssueListEndpoint(BaseAPIView):
permission_classes = [
@@ -114,7 +115,8 @@ class IssueListEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -308,7 +310,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py
index db6b5b9fb..e1c6962d8 100644
--- a/apiserver/plane/app/views/issue/draft.py
+++ b/apiserver/plane/app/views/issue/draft.py
@@ -101,7 +101,8 @@ class IssueDraftViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py
index 6ec4a2de1..da479e0e9 100644
--- a/apiserver/plane/app/views/issue/sub_issue.py
+++ b/apiserver/plane/app/views/issue/sub_issue.py
@@ -83,7 +83,8 @@ class SubIssuesEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py
index f6329c223..39dbcb751 100644
--- a/apiserver/plane/app/views/module/base.py
+++ b/apiserver/plane/app/views/module/base.py
@@ -86,6 +86,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
),
)
.annotate(
@@ -96,6 +97,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -106,6 +108,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -116,6 +119,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -126,6 +130,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -136,6 +141,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -492,10 +498,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
workspace__slug=self.kwargs.get("slug"),
)
return (
- super()
- .get_queryset()
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(workspace__slug=self.kwargs.get("slug"))
+ Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(archived_at__isnull=False)
.annotate(is_favorite=Exists(favorite_subquery))
.select_related("project")
@@ -517,6 +520,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
),
)
.annotate(
@@ -527,6 +531,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -537,6 +542,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -547,6 +553,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -557,6 +564,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -567,6 +575,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -582,7 +591,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
.order_by("-is_favorite", "-created_at")
)
- def list(self, request, slug, project_id):
+ def get(self, request, slug, project_id):
queryset = self.get_queryset()
modules = queryset.values( # Required fields
"id",
@@ -612,6 +621,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"backlog_issues",
"created_at",
"updated_at",
+ "archived_at"
)
return Response(modules, status=status.HTTP_200_OK)
diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py
index cfa8ee478..d26433340 100644
--- a/apiserver/plane/app/views/module/issue.py
+++ b/apiserver/plane/app/views/module/issue.py
@@ -93,7 +93,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py
index e2fc29aac..45e7bd29c 100644
--- a/apiserver/plane/app/views/view/base.py
+++ b/apiserver/plane/app/views/view/base.py
@@ -125,7 +125,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/apiserver/plane/app/views/workspace/module.py b/apiserver/plane/app/views/workspace/module.py
index fbd760271..8dd5e97f4 100644
--- a/apiserver/plane/app/views/workspace/module.py
+++ b/apiserver/plane/app/views/workspace/module.py
@@ -45,6 +45,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
),
)
.annotate(
@@ -55,6 +56,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -65,6 +67,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -75,6 +78,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -85,6 +89,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.annotate(
@@ -95,6 +100,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
+ distinct=True,
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py
index fe495de6c..94a22a1a7 100644
--- a/apiserver/plane/app/views/workspace/user.py
+++ b/apiserver/plane/app/views/workspace/user.py
@@ -165,7 +165,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
- filter=~Q(assignees__id__isnull=True),
+ filter=~Q(assignees__id__isnull=True)
+ & Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts
index c41ab279b..30724706b 100644
--- a/packages/types/src/cycle/cycle.d.ts
+++ b/packages/types/src/cycle/cycle.d.ts
@@ -31,6 +31,7 @@ export interface ICycle {
unstarted_issues: number;
updated_at: Date;
updated_by: string;
+ archived_at: string | null;
assignee_ids: string[];
view_props: {
filters: IIssueFilterOptions;
diff --git a/packages/types/src/cycle/cycle_filters.d.ts b/packages/types/src/cycle/cycle_filters.d.ts
index 470a20dd2..38f8a7549 100644
--- a/packages/types/src/cycle/cycle_filters.d.ts
+++ b/packages/types/src/cycle/cycle_filters.d.ts
@@ -13,6 +13,11 @@ export type TCycleFilters = {
status?: string[] | null;
};
+export type TCycleFiltersByState = {
+ default: TCycleFilters;
+ archived: TCycleFilters;
+};
+
export type TCycleStoredFilters = {
display_filters?: TCycleDisplayFilters;
filters?: TCycleFilters;
diff --git a/packages/types/src/module/module_filters.d.ts b/packages/types/src/module/module_filters.d.ts
index 10d56c328..297c8046c 100644
--- a/packages/types/src/module/module_filters.d.ts
+++ b/packages/types/src/module/module_filters.d.ts
@@ -26,6 +26,11 @@ export type TModuleFilters = {
target_date?: string[] | null;
};
+export type TModuleFiltersByState = {
+ default: TModuleFilters;
+ archived: TModuleFilters;
+};
+
export type TModuleStoredFilters = {
display_filters?: TModuleDisplayFilters;
filters?: TModuleFilters;
diff --git a/packages/types/src/module/modules.d.ts b/packages/types/src/module/modules.d.ts
index 0af293e50..7ba2c3b41 100644
--- a/packages/types/src/module/modules.d.ts
+++ b/packages/types/src/module/modules.d.ts
@@ -39,6 +39,7 @@ export interface IModule {
unstarted_issues: number;
updated_at: Date;
updated_by: string;
+ archived_at: string | null;
view_props: {
filters: IIssueFilterOptions;
};
diff --git a/web/components/archives/archive-tabs-list.tsx b/web/components/archives/archive-tabs-list.tsx
new file mode 100644
index 000000000..57d1c36a1
--- /dev/null
+++ b/web/components/archives/archive-tabs-list.tsx
@@ -0,0 +1,43 @@
+import { FC } from "react";
+import { observer } from "mobx-react";
+import Link from "next/link";
+import { useRouter } from "next/router";
+// constants
+import { ARCHIVES_TAB_LIST } from "@/constants/archives";
+// hooks
+import { useProject } from "@/hooks/store";
+
+export const ArchiveTabsList: FC = observer(() => {
+ // router
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
+ const activeTab = router.pathname.split("/").pop();
+ // store hooks
+ const { getProjectById } = useProject();
+
+ // derived values
+ if (!projectId) return null;
+ const projectDetails = getProjectById(projectId?.toString());
+ if (!projectDetails) return null;
+
+ return (
+ <>
+ {ARCHIVES_TAB_LIST.map(
+ (tab) =>
+ tab.shouldRender(projectDetails) && (
+
+
+ {tab.label}
+
+
+ )
+ )}
+ >
+ );
+});
diff --git a/web/components/archives/index.ts b/web/components/archives/index.ts
new file mode 100644
index 000000000..4b519fca0
--- /dev/null
+++ b/web/components/archives/index.ts
@@ -0,0 +1 @@
+export * from "./archive-tabs-list";
diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx
index 9556eb1aa..83db67c34 100644
--- a/web/components/core/sidebar/links-list.tsx
+++ b/web/components/core/sidebar/links-list.tsx
@@ -16,12 +16,13 @@ type Props = {
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: ILinkDetails) => void;
userAuth: UserAuth;
+ disabled?: boolean;
};
-export const LinksList: React.FC = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
+export const LinksList: React.FC = observer(({ links, handleDeleteLink, handleEditLink, userAuth, disabled }) => {
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
- const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
+ const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx
index a1c5f3a13..68b1708fe 100644
--- a/web/components/core/sidebar/progress-chart.tsx
+++ b/web/components/core/sidebar/progress-chart.tsx
@@ -12,6 +12,7 @@ type Props = {
startDate: string | Date;
endDate: string | Date;
totalIssues: number;
+ className?: string;
};
const styleById = {
@@ -40,7 +41,7 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
/>
));
-const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues }) => {
+const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues, className = "" }) => {
const chartData = Object.keys(distribution ?? []).map((key) => ({
currentDate: renderFormattedDateWithoutYear(key),
pending: distribution[key],
@@ -73,7 +74,7 @@ const ProgressChart: React.FC = ({ distribution, startDate, endDate, tota
};
return (
-
+
= observer((props) => {
+ const { workspaceSlug, projectId, cycle } = props;
+
+ const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
+
+ const currentValue = (tab: string | null) => {
+ switch (tab) {
+ case "Priority-Issues":
+ return 0;
+ case "Assignees":
+ return 1;
+ case "Labels":
+ return 2;
+ default:
+ return 0;
+ }
+ };
+ const {
+ issues: { fetchActiveCycleIssues },
+ } = useIssues(EIssuesStoreType.CYCLE);
+
+ const { currentProjectDetails } = useProject();
+
+ const { data: activeCycleIssues } = useSWR(
+ workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null,
+ workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, cycle.id) : null
+ );
+
+ const cycleIssues = activeCycleIssues ?? [];
+
+ return (
+
+
{
+ switch (i) {
+ case 0:
+ return setTab("Priority-Issues");
+ case 1:
+ return setTab("Assignees");
+ case 2:
+ return setTab("Labels");
+
+ default:
+ return setTab("Priority-Issues");
+ }
+ }}
+ >
+
+
+ cn(
+ "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
+ {
+ "text-custom-text-300 bg-custom-background-100": selected,
+ "hover:text-custom-text-300": !selected,
+ }
+ )
+ }
+ >
+ Priority Issues
+
+
+ cn(
+ "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
+ {
+ "text-custom-text-300 bg-custom-background-100": selected,
+ "hover:text-custom-text-300": !selected,
+ }
+ )
+ }
+ >
+ Assignees
+
+
+ cn(
+ "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
+ {
+ "text-custom-text-300 bg-custom-background-100": selected,
+ "hover:text-custom-text-300": !selected,
+ }
+ )
+ }
+ >
+ Labels
+
+
+
+
+
+
+ {cycleIssues ? (
+ cycleIssues.length > 0 ? (
+ cycleIssues.map((issue: TIssue) => (
+
+
+
+
+
+
+ {currentProjectDetails?.identifier}-{issue.sequence_id}
+
+
+
+ {issue.name}
+
+
+
+
{}}
+ projectId={projectId?.toString() ?? ""}
+ disabled
+ buttonVariant="background-with-text"
+ buttonContainerClassName="cursor-pointer max-w-24"
+ showTooltip
+ />
+ {issue.target_date && (
+
+
+
+
+ {renderFormattedDateWithoutYear(issue.target_date)}
+
+
+
+ )}
+
+
+ ))
+ ) : (
+
+ There are no high priority issues present in this cycle.
+
+ )
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+
+ {cycle.distribution?.assignees?.map((assignee, index) => {
+ if (assignee.assignee_id)
+ return (
+
+
+
+ {assignee.display_name}
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
+ />
+ );
+ else
+ return (
+
+
+

+
+ No assignee
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
+ />
+ );
+ })}
+
+
+
+ {cycle.distribution?.labels?.map((label, index) => (
+
+
+ {label.label_name ?? "No labels"}
+
+ }
+ completed={label.completed_issues}
+ total={label.total_issues}
+ />
+ ))}
+
+
+
+
+ );
+});
diff --git a/web/components/cycles/active-cycle/header.tsx b/web/components/cycles/active-cycle/header.tsx
new file mode 100644
index 000000000..98ed91c1d
--- /dev/null
+++ b/web/components/cycles/active-cycle/header.tsx
@@ -0,0 +1,77 @@
+import { FC } from "react";
+import Link from "next/link";
+// types
+import { ICycle, TCycleGroups } from "@plane/types";
+// ui
+import { Tooltip, CycleGroupIcon, getButtonStyling, Avatar, AvatarGroup } from "@plane/ui";
+// helpers
+import { renderFormattedDate, findHowManyDaysLeft } from "@/helpers/date-time.helper";
+import { truncateText } from "@/helpers/string.helper";
+// hooks
+import { useMember } from "@/hooks/store";
+
+export type ActiveCycleHeaderProps = {
+ cycle: ICycle;
+ workspaceSlug: string;
+ projectId: string;
+};
+
+export const ActiveCycleHeader: FC = (props) => {
+ const { cycle, workspaceSlug, projectId } = props;
+ // store
+ const { getUserDetails } = useMember();
+ const cycleOwnerDetails = cycle && cycle.owned_by_id ? getUserDetails(cycle.owned_by_id) : undefined;
+
+ const daysLeft = findHowManyDaysLeft(cycle.end_date) ?? 0;
+ const currentCycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups;
+
+ const cycleAssignee = (cycle.distribution?.assignees ?? []).filter((assignee) => assignee.display_name);
+
+ return (
+
+
+
+
+ {truncateText(cycle.name, 70)}
+
+
+
+ {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
+
+
+
+
+
+
+
+ {cycleAssignee.length > 0 && (
+
+
+ {cycleAssignee.map((member) => (
+
+ ))}
+
+
+ )}
+
+
+
+ View Cycle
+
+
+
+ );
+};
diff --git a/web/components/cycles/active-cycle/index.ts b/web/components/cycles/active-cycle/index.ts
index 73d5d1e98..d88ccc3e8 100644
--- a/web/components/cycles/active-cycle/index.ts
+++ b/web/components/cycles/active-cycle/index.ts
@@ -1,4 +1,8 @@
export * from "./root";
+export * from "./header";
export * from "./stats";
export * from "./upcoming-cycles-list-item";
export * from "./upcoming-cycles-list";
+export * from "./cycle-stats";
+export * from "./progress";
+export * from "./productivity";
diff --git a/web/components/cycles/active-cycle/productivity.tsx b/web/components/cycles/active-cycle/productivity.tsx
new file mode 100644
index 000000000..59c2ac3c9
--- /dev/null
+++ b/web/components/cycles/active-cycle/productivity.tsx
@@ -0,0 +1,46 @@
+import { FC } from "react";
+// types
+import { ICycle } from "@plane/types";
+// components
+import ProgressChart from "@/components/core/sidebar/progress-chart";
+
+export type ActiveCycleProductivityProps = {
+ cycle: ICycle;
+};
+
+export const ActiveCycleProductivity: FC = (props) => {
+ const { cycle } = props;
+
+ return (
+
+
+
Issue burndown
+
+
+
+
+
+
+
+ Ideal
+
+
+
+ Current
+
+
+
{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}
+
+
+
+
+ );
+};
diff --git a/web/components/cycles/active-cycle/progress.tsx b/web/components/cycles/active-cycle/progress.tsx
new file mode 100644
index 000000000..dea3b496a
--- /dev/null
+++ b/web/components/cycles/active-cycle/progress.tsx
@@ -0,0 +1,79 @@
+import { FC } from "react";
+// types
+import { ICycle } from "@plane/types";
+// ui
+import { LinearProgressIndicator } from "@plane/ui";
+// constants
+import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
+
+export type ActiveCycleProgressProps = {
+ cycle: ICycle;
+};
+
+export const ActiveCycleProgress: FC = (props) => {
+ const { cycle } = props;
+
+ const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
+ id: index,
+ name: group.title,
+ value: cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0,
+ color: group.color,
+ }));
+
+ const groupedIssues: any = {
+ completed: cycle.completed_issues,
+ started: cycle.started_issues,
+ unstarted: cycle.unstarted_issues,
+ backlog: cycle.backlog_issues,
+ };
+
+ return (
+
+
+
+
Progress
+
+ {`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
+ cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
+ } closed`}
+
+
+
+
+
+
+ {Object.keys(groupedIssues).map((group, index) => (
+ <>
+ {groupedIssues[group] > 0 && (
+
+
+
+
+ {group}
+
+
{`${groupedIssues[group]} ${
+ groupedIssues[group] > 1 ? "Issues" : "Issue"
+ }`}
+
+
+ )}
+ >
+ ))}
+ {cycle.cancelled_issues > 0 && (
+
+
+ {`${cycle.cancelled_issues} cancelled ${
+ cycle.cancelled_issues > 1 ? "issues are" : "issue is"
+ } excluded from this report.`}{" "}
+
+
+ )}
+
+
+ );
+};
diff --git a/web/components/cycles/active-cycle/root.tsx b/web/components/cycles/active-cycle/root.tsx
index 83acd1521..bd2c3b613 100644
--- a/web/components/cycles/active-cycle/root.tsx
+++ b/web/components/cycles/active-cycle/root.tsx
@@ -1,48 +1,20 @@
-import { MouseEvent } from "react";
import { observer } from "mobx-react-lite";
-import Link from "next/link";
import useSWR from "swr";
-// hooks
-import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
-import { ICycle, TCycleGroups } from "@plane/types";
-import {
- AvatarGroup,
- Loader,
- Tooltip,
- LinearProgressIndicator,
- LayersIcon,
- StateGroupIcon,
- PriorityIcon,
- Avatar,
- CycleGroupIcon,
- setPromiseToast,
- getButtonStyling,
-} from "@plane/ui";
-import { SingleProgressStats } from "@/components/core";
// ui
+import { Loader } from "@plane/ui";
// components
-import ProgressChart from "@/components/core/sidebar/progress-chart";
-import { ActiveCycleProgressStats, UpcomingCyclesList } from "@/components/cycles";
-import { StateDropdown } from "@/components/dropdowns";
-import { EmptyState } from "@/components/empty-state";
-// icons
-// helpers
-// types
-// constants
-import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
-import { EmptyStateType } from "@/constants/empty-state";
-import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
-import { EIssuesStoreType } from "@/constants/issue";
-import { cn } from "@/helpers/common.helper";
import {
- renderFormattedDate,
- findHowManyDaysLeft,
- renderFormattedDateWithoutYear,
- getDate,
-} from "@/helpers/date-time.helper";
-import { truncateText } from "@/helpers/string.helper";
-import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "@/hooks/store";
-import { usePlatformOS } from "@/hooks/use-platform-os";
+ ActiveCycleHeader,
+ ActiveCycleProductivity,
+ ActiveCycleProgress,
+ ActiveCycleStats,
+ UpcomingCyclesList,
+} from "@/components/cycles";
+import { EmptyState } from "@/components/empty-state";
+// constants
+import { EmptyStateType } from "@/constants/empty-state";
+// hooks
+import { useCycle, useCycleFilter } from "@/hooks/store";
interface IActiveCycleDetails {
workspaceSlug: string;
@@ -52,41 +24,24 @@ interface IActiveCycleDetails {
export const ActiveCycleRoot: React.FC = observer((props) => {
// props
const { workspaceSlug, projectId } = props;
- // hooks
- const { isMobile } = usePlatformOS();
// store hooks
- const {
- issues: { fetchActiveCycleIssues },
- } = useIssues(EIssuesStoreType.CYCLE);
- const {
- currentProjectActiveCycleId,
- currentProjectUpcomingCycleIds,
- fetchActiveCycle,
- getActiveCycleById,
- addCycleToFavorites,
- removeCycleFromFavorites,
- } = useCycle();
- const { currentProjectDetails } = useProject();
- const { getUserDetails } = useMember();
+ const { fetchActiveCycle, currentProjectActiveCycleId, currentProjectUpcomingCycleIds, getActiveCycleById } =
+ useCycle();
// cycle filters hook
const { updateDisplayFilters } = useCycleFilter();
// derived values
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
- const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by_id) : undefined;
// fetch active cycle details
const { isLoading } = useSWR(
workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
);
- // fetch active cycle issues
- const { data: activeCycleIssues } = useSWR(
- workspaceSlug && projectId && currentProjectActiveCycleId
- ? CYCLE_ISSUES_WITH_PARAMS(currentProjectActiveCycleId, { priority: "urgent,high" })
- : null,
- workspaceSlug && projectId && currentProjectActiveCycleId
- ? () => fetchActiveCycleIssues(workspaceSlug, projectId, currentProjectActiveCycleId)
- : null
- );
+
+ const handleEmptyStateAction = () =>
+ updateDisplayFilters(projectId, {
+ active_tab: "all",
+ });
+
// show loader if active cycle is loading
if (!activeCycle && isLoading)
return (
@@ -110,310 +65,28 @@ export const ActiveCycleRoot: React.FC = observer((props) =
Create new cycles to find them here or check
{"'"}All{"'"} cycles tab to see all cycles or{" "}
-
-
+
>
);
}
- const endDate = getDate(activeCycle.end_date);
- const startDate = getDate(activeCycle.start_date);
- const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
- const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;
-
- const groupedIssues: any = {
- backlog: activeCycle.backlog_issues,
- unstarted: activeCycle.unstarted_issues,
- started: activeCycle.started_issues,
- completed: activeCycle.completed_issues,
- cancelled: activeCycle.cancelled_issues,
- };
-
- const handleAddToFavorites = (e: MouseEvent) => {
- e.preventDefault();
- if (!workspaceSlug || !projectId) return;
-
- const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id);
-
- setPromiseToast(addToFavoritePromise, {
- loading: "Adding cycle to favorites...",
- success: {
- title: "Success!",
- message: () => "Cycle added to favorites.",
- },
- error: {
- title: "Error!",
- message: () => "Couldn't add the cycle to favorites. Please try again.",
- },
- });
- };
-
- const handleRemoveFromFavorites = (e: MouseEvent) => {
- e.preventDefault();
- if (!workspaceSlug || !projectId) return;
-
- const removeFromFavoritePromise = removeCycleFromFavorites(
- workspaceSlug?.toString(),
- projectId.toString(),
- activeCycle.id
- );
-
- setPromiseToast(removeFromFavoritePromise, {
- loading: "Removing cycle from favorites...",
- success: {
- title: "Success!",
- message: () => "Cycle removed from favorites.",
- },
- error: {
- title: "Error!",
- message: () => "Couldn't remove the cycle from favorites. Please try again.",
- },
- });
- };
-
- const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
- id: index,
- name: group.title,
- value:
- activeCycle.total_issues > 0
- ? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100
- : 0,
- color: group.color,
- }));
-
return (
-
-
-
-
-
-
-
-
-
-
-
- {truncateText(activeCycle.name, 70)}
-
-
-
-
- {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
-
- {activeCycle.is_favorite ? (
- {
- handleRemoveFromFavorites(e);
- }}
- >
-
-
- ) : (
- {
- handleAddToFavorites(e);
- }}
- >
-
-
- )}
-
-
-
-
-
-
- {renderFormattedDate(startDate)}
-
-
-
-
- {renderFormattedDate(endDate)}
-
-
-
-
-
-
-
{cycleOwnerDetails?.display_name}
-
-
- {activeCycle.assignee_ids.length > 0 && (
-
-
- {activeCycle.assignee_ids.map((assignee_id) => {
- const member = getUserDetails(assignee_id);
- return ;
- })}
-
-
- )}
-
-
-
-
-
- {activeCycle.total_issues} issues
-
-
-
- {activeCycle.completed_issues} issues
-
-
-
-
- View cycle
-
-
-
-
-
-
-
-
- Progress
-
-
-
- {Object.keys(groupedIssues).map((group, index) => (
-
-
- {group}
-
- }
- completed={groupedIssues[group]}
- total={activeCycle.total_issues}
- />
- ))}
-
-
-
-
+ <>
+
-
-
-
High priority issues
-
- {activeCycleIssues ? (
- activeCycleIssues.length > 0 ? (
- activeCycleIssues.map((issue) => (
-
-
-
-
-
-
- {currentProjectDetails?.identifier}-{issue.sequence_id}
-
-
-
- {truncateText(issue.name, 30)}
-
-
-
-
{}}
- projectId={projectId}
- disabled
- buttonVariant="background-with-text"
- />
- {issue.target_date && (
-
-
-
- {renderFormattedDateWithoutYear(issue.target_date)}
-
-
- )}
-
-
- ))
- ) : (
-
- There are no high priority issues present in this cycle.
-
- )
- ) : (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
- Ideal
-
-
-
- Current
-
-
-
-
-
-
-
- Pending issues-{" "}
- {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
-
-
-
-
-
-
-
+ {currentProjectUpcomingCycleIds &&
}
+ >
);
});
diff --git a/web/components/cycles/active-cycle/upcoming-cycles-list.tsx b/web/components/cycles/active-cycle/upcoming-cycles-list.tsx
index c2d8b2388..f4156f341 100644
--- a/web/components/cycles/active-cycle/upcoming-cycles-list.tsx
+++ b/web/components/cycles/active-cycle/upcoming-cycles-list.tsx
@@ -1,10 +1,16 @@
+import { FC } from "react";
import { observer } from "mobx-react";
-// hooks
-import { UpcomingCycleListItem } from "@/components/cycles";
-import { useCycle } from "@/hooks/store";
// components
+import { UpcomingCycleListItem } from "@/components/cycles";
+// hooks
+import { useCycle } from "@/hooks/store";
-export const UpcomingCyclesList = observer(() => {
+type Props = {
+ handleEmptyStateAction: () => void;
+};
+
+export const UpcomingCyclesList: FC
= observer((props) => {
+ const { handleEmptyStateAction } = props;
// store hooks
const { currentProjectUpcomingCycleIds } = useCycle();
@@ -12,14 +18,30 @@ export const UpcomingCyclesList = observer(() => {
return (
-
- Upcoming cycles
-
-
- {currentProjectUpcomingCycleIds.map((cycleId) => (
-
- ))}
+
+ Next cycles
+ {currentProjectUpcomingCycleIds.length > 0 ? (
+
+ {currentProjectUpcomingCycleIds.map((cycleId) => (
+
+ ))}
+
+ ) : (
+
+
+
No upcoming cycles
+
+ Create new cycles to find them here or check
+
+ {"'"}All{"'"} cycles tab to see all cycles or{" "}
+
+ click here
+
+
+
+
+ )}
);
});
diff --git a/web/components/cycles/archived-cycles/header.tsx b/web/components/cycles/archived-cycles/header.tsx
new file mode 100644
index 000000000..267c87388
--- /dev/null
+++ b/web/components/cycles/archived-cycles/header.tsx
@@ -0,0 +1,123 @@
+import { FC, useCallback, useRef, useState } from "react";
+import { observer } from "mobx-react-lite";
+import { useRouter } from "next/router";
+// icons
+import { ListFilter, Search, X } from "lucide-react";
+// types
+import type { TCycleFilters } from "@plane/types";
+// components
+import { ArchiveTabsList } from "@/components/archives";
+import { CycleFiltersSelection } from "@/components/cycles";
+import { FiltersDropdown } from "@/components/issues";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { useCycleFilter } from "@/hooks/store";
+import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
+
+export const ArchivedCyclesHeader: FC = observer(() => {
+ // router
+ const router = useRouter();
+ const { projectId } = router.query;
+ // refs
+ const inputRef = useRef
(null);
+ // hooks
+ const { currentProjectArchivedFilters, archivedCyclesSearchQuery, updateFilters, updateArchivedCyclesSearchQuery } =
+ useCycleFilter();
+ // states
+ const [isSearchOpen, setIsSearchOpen] = useState(archivedCyclesSearchQuery !== "" ? true : false);
+ // outside click detector hook
+ useOutsideClickDetector(inputRef, () => {
+ if (isSearchOpen && archivedCyclesSearchQuery.trim() === "") setIsSearchOpen(false);
+ });
+
+ const handleFilters = useCallback(
+ (key: keyof TCycleFilters, value: string | string[]) => {
+ if (!projectId) return;
+
+ const newValues = currentProjectArchivedFilters?.[key] ?? [];
+
+ if (Array.isArray(value))
+ value.forEach((val) => {
+ if (!newValues.includes(val)) newValues.push(val);
+ });
+ else {
+ if (currentProjectArchivedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
+ else newValues.push(value);
+ }
+
+ updateFilters(projectId.toString(), { [key]: newValues }, "archived");
+ },
+ [currentProjectArchivedFilters, projectId, updateFilters]
+ );
+
+ const handleInputKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Escape") {
+ if (archivedCyclesSearchQuery && archivedCyclesSearchQuery.trim() !== "") updateArchivedCyclesSearchQuery("");
+ else {
+ setIsSearchOpen(false);
+ inputRef.current?.blur();
+ }
+ }
+ };
+
+ return (
+
+
+ {/* filter options */}
+
+ {!isSearchOpen && (
+
{
+ setIsSearchOpen(true);
+ inputRef.current?.focus();
+ }}
+ >
+
+
+ )}
+
+
+ updateArchivedCyclesSearchQuery(e.target.value)}
+ onKeyDown={handleInputKeyDown}
+ />
+ {isSearchOpen && (
+ {
+ updateArchivedCyclesSearchQuery("");
+ setIsSearchOpen(false);
+ }}
+ >
+
+
+ )}
+
+
} title="Filters" placement="bottom-end">
+
+
+
+
+ );
+});
diff --git a/web/components/cycles/archived-cycles/index.ts b/web/components/cycles/archived-cycles/index.ts
new file mode 100644
index 000000000..f59f0954e
--- /dev/null
+++ b/web/components/cycles/archived-cycles/index.ts
@@ -0,0 +1,4 @@
+export * from "./root";
+export * from "./view";
+export * from "./header";
+export * from "./modal";
diff --git a/web/components/cycles/archived-cycles/modal.tsx b/web/components/cycles/archived-cycles/modal.tsx
new file mode 100644
index 000000000..a9b421351
--- /dev/null
+++ b/web/components/cycles/archived-cycles/modal.tsx
@@ -0,0 +1,104 @@
+import { useState, Fragment } from "react";
+import { useRouter } from "next/router";
+import { Dialog, Transition } from "@headlessui/react";
+// ui
+import { Button, TOAST_TYPE, setToast } from "@plane/ui";
+// hooks
+import { useCycle } from "@/hooks/store";
+
+type Props = {
+ workspaceSlug: string;
+ projectId: string;
+ cycleId: string;
+ handleClose: () => void;
+ isOpen: boolean;
+ onSubmit?: () => Promise;
+};
+
+export const ArchiveCycleModal: React.FC = (props) => {
+ const { workspaceSlug, projectId, cycleId, isOpen, handleClose } = props;
+ // router
+ const router = useRouter();
+ // states
+ const [isArchiving, setIsArchiving] = useState(false);
+ // store hooks
+ const { getCycleNameById, archiveCycle } = useCycle();
+
+ const cycleName = getCycleNameById(cycleId);
+
+ const onClose = () => {
+ setIsArchiving(false);
+ handleClose();
+ };
+
+ const handleArchiveIssue = async () => {
+ setIsArchiving(true);
+ await archiveCycle(workspaceSlug, projectId, cycleId)
+ .then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Archive success",
+ message: "Your archives can be found in project archives.",
+ });
+ onClose();
+ router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles?peekCycle=${cycleId}`);
+ })
+ .catch(() =>
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Cycle could not be archived. Please try again.",
+ })
+ )
+ .finally(() => setIsArchiving(false));
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/web/components/cycles/archived-cycles/root.tsx b/web/components/cycles/archived-cycles/root.tsx
new file mode 100644
index 000000000..4d47c8f34
--- /dev/null
+++ b/web/components/cycles/archived-cycles/root.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import { observer } from "mobx-react-lite";
+import { useRouter } from "next/router";
+import useSWR from "swr";
+// types
+import { TCycleFilters } from "@plane/types";
+// components
+import { ArchivedCyclesView, CycleAppliedFiltersList } from "@/components/cycles";
+import { EmptyState } from "@/components/empty-state";
+import { CycleModuleListLayout } from "@/components/ui";
+// constants
+import { EmptyStateType } from "@/constants/empty-state";
+// helpers
+import { calculateTotalFilters } from "@/helpers/filter.helper";
+// hooks
+import { useCycle, useCycleFilter } from "@/hooks/store";
+
+export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
+ // router
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
+ // hooks
+ const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle();
+ // cycle filters hook
+ const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter();
+ // derived values
+ const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0;
+
+ useSWR(
+ workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
+ async () => {
+ if (workspaceSlug && projectId) {
+ await fetchArchivedCycles(workspaceSlug.toString(), projectId.toString());
+ }
+ },
+ { revalidateIfStale: false, revalidateOnFocus: false }
+ );
+
+ const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
+ if (!projectId) return;
+ let newValues = currentProjectArchivedFilters?.[key] ?? [];
+
+ if (!value) newValues = [];
+ else newValues = newValues.filter((val) => val !== value);
+
+ updateFilters(projectId.toString(), { [key]: newValues }, "archived");
+ };
+
+ if (!workspaceSlug || !projectId) return <>>;
+
+ if (loader || !currentProjectArchivedCycleIds) {
+ return ;
+ }
+
+ return (
+ <>
+ {calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0 && (
+
+ clearAllFilters(projectId.toString(), "archived")}
+ handleRemoveFilter={handleRemoveFilter}
+ />
+
+ )}
+ {totalArchivedCycles === 0 ? (
+
+
+
+ ) : (
+
+ )}
+ >
+ );
+});
diff --git a/web/components/cycles/archived-cycles/view.tsx b/web/components/cycles/archived-cycles/view.tsx
new file mode 100644
index 000000000..ed86a56b4
--- /dev/null
+++ b/web/components/cycles/archived-cycles/view.tsx
@@ -0,0 +1,57 @@
+import { FC } from "react";
+import { observer } from "mobx-react-lite";
+import Image from "next/image";
+// components
+import { CyclesList } from "@/components/cycles";
+// ui
+import { CycleModuleListLayout } from "@/components/ui";
+// hooks
+import { useCycle, useCycleFilter } from "@/hooks/store";
+// assets
+import AllFiltersImage from "@/public/empty-state/cycle/all-filters.svg";
+import NameFilterImage from "@/public/empty-state/cycle/name-filter.svg";
+
+export interface IArchivedCyclesView {
+ workspaceSlug: string;
+ projectId: string;
+}
+
+export const ArchivedCyclesView: FC = observer((props) => {
+ const { workspaceSlug, projectId } = props;
+ // store hooks
+ const { getFilteredArchivedCycleIds, loader } = useCycle();
+ const { archivedCyclesSearchQuery } = useCycleFilter();
+ // derived values
+ const filteredArchivedCycleIds = getFilteredArchivedCycleIds(projectId);
+
+ if (loader || !filteredArchivedCycleIds) return ;
+
+ if (filteredArchivedCycleIds.length === 0)
+ return (
+
+
+
+
No matching cycles
+
+ {archivedCyclesSearchQuery.trim() === ""
+ ? "Remove the filters to see all cycles"
+ : "Remove the search criteria to see all cycles"}
+
+
+
+ );
+
+ return (
+
+ );
+});
diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx
index 4b88d8d7b..8409c06fe 100644
--- a/web/components/cycles/cycle-peek-overview.tsx
+++ b/web/components/cycles/cycle-peek-overview.tsx
@@ -9,9 +9,10 @@ import { CycleDetailsSidebar } from "./sidebar";
type Props = {
projectId: string;
workspaceSlug: string;
+ isArchived?: boolean;
};
-export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug }) => {
+export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug, isArchived = false }) => {
// router
const router = useRouter();
const { peekCycle } = router.query;
@@ -29,9 +30,9 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa
};
useEffect(() => {
- if (!peekCycle) return;
+ if (!peekCycle || isArchived) return;
fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
- }, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]);
+ }, [fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
return (
<>
@@ -44,7 +45,11 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
-
+
)}
>
diff --git a/web/components/cycles/cycles-view-header.tsx b/web/components/cycles/cycles-view-header.tsx
index c687c965e..aad650dd6 100644
--- a/web/components/cycles/cycles-view-header.tsx
+++ b/web/components/cycles/cycles-view-header.tsx
@@ -2,21 +2,21 @@ import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import { ListFilter, Search, X } from "lucide-react";
import { Tab } from "@headlessui/react";
+// types
import { TCycleFilters } from "@plane/types";
-// hooks
+// ui
import { Tooltip } from "@plane/ui";
+// components
import { CycleFiltersSelection } from "@/components/cycles";
import { FiltersDropdown } from "@/components/issues";
+// constants
import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle";
+// helpers
import { cn } from "@/helpers/common.helper";
+// hooks
import { useCycleFilter } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
-// components
-// ui
-// helpers
-// types
-// constants
type Props = {
projectId: string;
@@ -24,8 +24,6 @@ type Props = {
export const CyclesViewHeader: React.FC = observer((props) => {
const { projectId } = props;
- // states
- const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
const inputRef = useRef(null);
// hooks
@@ -38,6 +36,8 @@ export const CyclesViewHeader: React.FC = observer((props) => {
updateSearchQuery,
} = useCycleFilter();
const { isMobile } = usePlatformOS();
+ // states
+ const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
diff --git a/web/components/cycles/dropdowns/filters/root.tsx b/web/components/cycles/dropdowns/filters/root.tsx
index 768b8a5dc..57e9ec90c 100644
--- a/web/components/cycles/dropdowns/filters/root.tsx
+++ b/web/components/cycles/dropdowns/filters/root.tsx
@@ -9,10 +9,11 @@ import { FilterEndDate, FilterStartDate, FilterStatus } from "@/components/cycle
type Props = {
filters: TCycleFilters;
handleFiltersUpdate: (key: keyof TCycleFilters, value: string | string[]) => void;
+ isArchived?: boolean;
};
export const CycleFiltersSelection: React.FC = observer((props) => {
- const { filters, handleFiltersUpdate } = props;
+ const { filters, handleFiltersUpdate, isArchived = false } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
@@ -38,13 +39,15 @@ export const CycleFiltersSelection: React.FC = observer((props) => {
{/* cycle status */}
-
- handleFiltersUpdate("status", val)}
- searchQuery={filtersSearchQuery}
- />
-
+ {!isArchived && (
+
+ handleFiltersUpdate("status", val)}
+ searchQuery={filtersSearchQuery}
+ />
+
+ )}
{/* start date */}
diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx
index 27b972044..f8092f8d0 100644
--- a/web/components/cycles/form.tsx
+++ b/web/components/cycles/form.tsx
@@ -113,7 +113,7 @@ export const CycleForm: React.FC
= (props) => {
id="cycle_description"
name="description"
placeholder="Description..."
- className="!h-24 w-full resize-none text-sm"
+ className="w-full text-sm resize-none min-h-24"
hasError={Boolean(errors?.description)}
value={value}
onChange={onChange}
diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts
index e37d266b7..b1b718175 100644
--- a/web/components/cycles/index.ts
+++ b/web/components/cycles/index.ts
@@ -14,3 +14,6 @@ export * from "./quick-actions";
export * from "./sidebar";
export * from "./transfer-issues-modal";
export * from "./transfer-issues";
+
+// archived cycles
+export * from "./archived-cycles";
diff --git a/web/components/cycles/list/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx
index 6e81da3c7..a418f9b04 100644
--- a/web/components/cycles/list/cycles-list-item.tsx
+++ b/web/components/cycles/list/cycles-list-item.tsx
@@ -2,27 +2,21 @@ import { FC, MouseEvent } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
-// hooks
-import { Check, Info, Star, User2 } from "lucide-react";
-import type { TCycleGroups } from "@plane/types";
-import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
-import { CycleQuickActions } from "@/components/cycles";
-// components
-// import { CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
-// ui
// icons
-// helpers
+import { Check, Info, Star, User2 } from "lucide-react";
+// types
+import type { TCycleGroups } from "@plane/types";
+// ui
+import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
+// components
+import { CycleQuickActions } from "@/components/cycles";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
-// components
-// ui
-// icons
-// helpers
-// constants
-// types
import { EUserProjectRoles } from "@/constants/project";
+// helpers
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
+// hooks
import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -34,10 +28,11 @@ type TCyclesListItem = {
handleRemoveFromFavorites?: () => void;
workspaceSlug: string;
projectId: string;
+ isArchived?: boolean;
};
export const CyclesListItem: FC = observer((props) => {
- const { cycleId, workspaceSlug, projectId } = props;
+ const { cycleId, workspaceSlug, projectId, isArchived } = props;
// router
const router = useRouter();
// hooks
@@ -106,7 +101,7 @@ export const CyclesListItem: FC = observer((props) => {
});
};
- const openCycleOverview = (e: MouseEvent) => {
+ const openCycleOverview = (e: MouseEvent) => {
const { query } = router;
e.preventDefault();
e.stopPropagation();
@@ -151,7 +146,14 @@ export const CyclesListItem: FC = observer((props) => {
return (
<>
-
+ {
+ if (isArchived) {
+ openCycleOverview(e);
+ }
+ }}
+ >
@@ -221,21 +223,23 @@ export const CyclesListItem: FC = observer((props) => {
- {isEditingAllowed && (
- <>
- {cycleDetails.is_favorite ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- >
- )}
+ {isEditingAllowed &&
+ !isArchived &&
+ (cycleDetails.is_favorite ? (
+
+
+
+ ) : (
+
+
+
+ ))}
+
diff --git a/web/components/cycles/list/cycles-list-map.tsx b/web/components/cycles/list/cycles-list-map.tsx
index 004c66fca..7a99f5ab7 100644
--- a/web/components/cycles/list/cycles-list-map.tsx
+++ b/web/components/cycles/list/cycles-list-map.tsx
@@ -5,15 +5,22 @@ type Props = {
cycleIds: string[];
projectId: string;
workspaceSlug: string;
+ isArchived?: boolean;
};
export const CyclesListMap: React.FC
= (props) => {
- const { cycleIds, projectId, workspaceSlug } = props;
+ const { cycleIds, projectId, workspaceSlug, isArchived } = props;
return (
<>
{cycleIds.map((cycleId) => (
-
+
))}
>
);
diff --git a/web/components/cycles/list/root.tsx b/web/components/cycles/list/root.tsx
index 904daa1d9..ef05228ee 100644
--- a/web/components/cycles/list/root.tsx
+++ b/web/components/cycles/list/root.tsx
@@ -12,16 +12,22 @@ export interface ICyclesList {
cycleIds: string[];
workspaceSlug: string;
projectId: string;
+ isArchived?: boolean;
}
export const CyclesList: FC = observer((props) => {
- const { completedCycleIds, cycleIds, workspaceSlug, projectId } = props;
+ const { completedCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props;
return (
-
+
{completedCycleIds.length !== 0 && (
@@ -37,12 +43,17 @@ export const CyclesList: FC = observer((props) => {
)}
-
+
)}
-
+
);
diff --git a/web/components/cycles/quick-actions.tsx b/web/components/cycles/quick-actions.tsx
index eebd28a9f..215f07bef 100644
--- a/web/components/cycles/quick-actions.tsx
+++ b/web/components/cycles/quick-actions.tsx
@@ -1,34 +1,40 @@
import { useState } from "react";
import { observer } from "mobx-react";
-import { LinkIcon, Pencil, Trash2 } from "lucide-react";
-// hooks
-// components
-import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
-import { CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
+import { useRouter } from "next/router";
+// icons
+import { ArchiveRestoreIcon, LinkIcon, Pencil, Trash2 } from "lucide-react";
// ui
-// helpers
-import { EUserProjectRoles } from "@/constants/project";
-import { copyUrlToClipboard } from "@/helpers/string.helper";
+import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
+// components
+import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
// constants
+import { EUserProjectRoles } from "@/constants/project";
+// helpers
+import { copyUrlToClipboard } from "@/helpers/string.helper";
+// hooks
import { useCycle, useEventTracker, useUser } from "@/hooks/store";
type Props = {
cycleId: string;
projectId: string;
workspaceSlug: string;
+ isArchived?: boolean;
};
export const CycleQuickActions: React.FC = observer((props) => {
- const { cycleId, projectId, workspaceSlug } = props;
+ const { cycleId, projectId, workspaceSlug, isArchived } = props;
+ // router
+ const router = useRouter();
// states
const [updateModal, setUpdateModal] = useState(false);
+ const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const {
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
- const { getCycleById } = useCycle();
+ const { getCycleById, restoreCycle } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const isCompleted = cycleDetails?.status.toLowerCase() === "completed";
@@ -56,6 +62,33 @@ export const CycleQuickActions: React.FC = observer((props) => {
setUpdateModal(true);
};
+ const handleArchiveCycle = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setArchiveCycleModal(true);
+ };
+
+ const handleRestoreCycle = async (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ await restoreCycle(workspaceSlug, projectId, cycleId)
+ .then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Restore success",
+ message: "Your cycle can be found in project cycles.",
+ });
+ router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
+ })
+ .catch(() =>
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Cycle could not be restored. Please try again.",
+ })
+ );
+ };
+
const handleDeleteCycle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -74,6 +107,13 @@ export const CycleQuickActions: React.FC = observer((props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
+ setArchiveCycleModal(false)}
+ />
= observer((props) => {
)}
+ {!isCompleted && isEditingAllowed && !isArchived && (
+
+
+
+ Edit cycle
+
+
+ )}
+ {isEditingAllowed && !isArchived && (
+
+ {isCompleted ? (
+
+ ) : (
+
+
+
+
Archive cycle
+
+ Only completed cycle
can be archived.
+
+
+
+ )}
+
+ )}
+ {isEditingAllowed && isArchived && (
+
+
+
+ Restore cycle
+
+
+ )}
+ {!isArchived && (
+
+
+
+ Copy cycle link
+
+
+ )}
{!isCompleted && isEditingAllowed && (
- <>
-
-
-
- Edit cycle
-
-
+
Delete cycle
- >
+
)}
-
-
-
- Copy cycle link
-
-
>
);
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx
index e142b95ec..e333564ee 100644
--- a/web/components/cycles/sidebar.tsx
+++ b/web/components/cycles/sidebar.tsx
@@ -3,33 +3,43 @@ import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
-import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react";
-import { Disclosure, Transition } from "@headlessui/react";
// icons
+import {
+ ArchiveRestoreIcon,
+ ChevronDown,
+ LinkIcon,
+ Trash2,
+ UserCircle2,
+ AlertCircle,
+ ChevronRight,
+ CalendarClock,
+} from "lucide-react";
+import { Disclosure, Transition } from "@headlessui/react";
+// types
import { ICycle } from "@plane/types";
// ui
-import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
+import { Avatar, ArchiveIcon, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
// components
import { SidebarProgressStats } from "@/components/core";
import ProgressChart from "@/components/core/sidebar/progress-chart";
-import { CycleDeleteModal } from "@/components/cycles/delete-modal";
+import { ArchiveCycleModal, CycleDeleteModal } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_UPDATED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
-// hooks
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
+// hooks
import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store";
// services
import { CycleService } from "@/services/cycle.service";
-// types
type Props = {
cycleId: string;
handleClose: () => void;
+ isArchived?: boolean;
};
const defaultValues: Partial = {
@@ -42,8 +52,9 @@ const cycleService = new CycleService();
// TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC = observer((props) => {
- const { cycleId, handleClose } = props;
+ const { cycleId, handleClose, isArchived } = props;
// states
+ const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// router
const router = useRouter();
@@ -53,7 +64,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
const {
membership: { currentProjectRole },
} = useUser();
- const { getCycleById, updateCycleDetails } = useCycle();
+ const { getCycleById, updateCycleDetails, restoreCycle } = useCycle();
const { getUserDetails } = useMember();
// derived values
const cycleDetails = getCycleById(cycleId);
@@ -108,6 +119,27 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
});
};
+ const handleRestoreCycle = async () => {
+ if (!workspaceSlug || !projectId) return;
+
+ await restoreCycle(workspaceSlug.toString(), projectId.toString(), cycleId)
+ .then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Restore success",
+ message: "Your cycle can be found in project cycles.",
+ });
+ router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/cycles/${cycleId}`);
+ })
+ .catch(() =>
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Cycle could not be restored. Please try again.",
+ })
+ );
+ };
+
useEffect(() => {
if (cycleDetails)
reset({
@@ -219,8 +251,8 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
? "0 Issue"
: `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
: cycleDetails.total_issues === 0
- ? "0 Issue"
- : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
+ ? "0 Issue"
+ : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
@@ -229,13 +261,22 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
return (
{cycleDetails && workspaceSlug && projectId && (
-
setCycleDeleteModal(false)}
- workspaceSlug={workspaceSlug.toString()}
- projectId={projectId.toString()}
- />
+ <>
+ setArchiveCycleModal(false)}
+ />
+ setCycleDeleteModal(false)}
+ workspaceSlug={workspaceSlug.toString()}
+ projectId={projectId.toString()}
+ />
+ >
)}
<>
@@ -249,22 +290,54 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
-
-
- {!isCompleted && isEditingAllowed && (
+ {!isArchived && (
+
+
+
+ )}
+ {isEditingAllowed && (
- {
- setTrackElement("CYCLE_PAGE_SIDEBAR");
- setCycleDeleteModal(true);
- }}
- >
-
-
- Delete cycle
-
-
+ {!isArchived && (
+ setArchiveCycleModal(true)} disabled={!isCompleted}>
+ {isCompleted ? (
+
+ ) : (
+
+
+
+
Archive cycle
+
+ Only completed cycle
can be archived.
+
+
+
+ )}
+
+ )}
+ {isArchived && (
+
+
+
+ Restore cycle
+
+
+ )}
+ {!isCompleted && (
+ {
+ setTrackElement("CYCLE_PAGE_SIDEBAR");
+ setCycleDeleteModal(true);
+ }}
+ >
+
+
+ Delete cycle
+
+
+ )}
)}
@@ -290,9 +363,11 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
{cycleDetails.description && (
-
- {cycleDetails.description}
-
+
)}
@@ -329,6 +404,7 @@ export const CycleDetailsSidebar: React.FC
= observer((props) => {
to: "End date",
}}
required={cycleDetails.status !== "draft"}
+ disabled={isArchived}
/>
)}
/>
diff --git a/web/components/dropdowns/date-range.tsx b/web/components/dropdowns/date-range.tsx
index 0ab33636d..8ae5726a8 100644
--- a/web/components/dropdowns/date-range.tsx
+++ b/web/components/dropdowns/date-range.tsx
@@ -149,6 +149,7 @@ export const DateRangeDropdown: React.FC = (props) => {
if (!isOpen) handleKeyDown(e);
} else handleKeyDown(e);
}}
+ disabled={disabled}
>
{
- // states
- const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
const inputRef = useRef(null);
// router
@@ -49,6 +47,8 @@ export const ModulesListHeader: React.FC = observer(() => {
updateFilters,
updateSearchQuery,
} = useModuleFilter();
+ // states
+ const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx
index 20258cd26..9182a6314 100644
--- a/web/components/headers/project-archived-issue-details.tsx
+++ b/web/components/headers/project-archived-issue-details.tsx
@@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import useSWR from "swr";
// hooks
-import { Breadcrumbs, LayersIcon } from "@plane/ui";
+import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { ProjectLogo } from "@/components/project";
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
@@ -39,7 +39,7 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
);
return (
-
+
@@ -59,18 +59,26 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
/>
}
/>
-
}
+ />
+ }
+ />
+ }
/>
}
/>
-
{
+ // router
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
+ const activeTab = router.pathname.split("/").pop();
+ // store hooks
+ const {
+ issuesFilter: { issueFilters },
+ } = useIssues(EIssuesStoreType.ARCHIVED);
+ const { currentProjectDetails } = useProject();
+ // hooks
+ const { isMobile } = usePlatformOS();
+
+ const issueCount = currentProjectDetails
+ ? issueFilters?.displayFilters?.sub_issue
+ ? currentProjectDetails.archived_issues + currentProjectDetails.archived_sub_issues
+ : currentProjectDetails.archived_issues
+ : undefined;
+
+ const activeTabBreadcrumbDetail =
+ PROJECT_ARCHIVES_BREADCRUMB_LIST[activeTab as keyof typeof PROJECT_ARCHIVES_BREADCRUMB_LIST];
+
+ return (
+
+
+
+
+
+
+
+ )
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ {activeTabBreadcrumbDetail && (
+ }
+ />
+ }
+ />
+ )}
+
+ {activeTab === "issues" && issueCount && issueCount > 0 ? (
+
1 ? "issues" : "issue"} in project's archived`}
+ position="bottom"
+ >
+
+ {issueCount}
+
+
+ ) : null}
+
+
+
+ );
+});
diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/issues/archived-issues-header.tsx
similarity index 55%
rename from web/components/headers/project-archived-issues.tsx
rename to web/components/issues/archived-issues-header.tsx
index 621f23eb2..cd5aca903 100644
--- a/web/components/headers/project-archived-issues.tsx
+++ b/web/components/issues/archived-issues-header.tsx
@@ -1,22 +1,17 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
-import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
-// hooks
-// constants
-// ui
-import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
-// components
-import { BreadcrumbLink } from "@/components/common";
-import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
-import { ProjectLogo } from "@/components/project";
-import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
-// helpers
-import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
// types
-import { usePlatformOS } from "@/hooks/use-platform-os";
+import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
+// components
+import { ArchiveTabsList } from "@/components/archives";
+import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
+// constants
+import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
+// hooks
+import { useIssues, useLabel, useMember, useProjectState } from "@/hooks/store";
-export const ProjectArchivedIssuesHeader: FC = observer(() => {
+export const ArchivedIssuesHeader: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -24,7 +19,6 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.ARCHIVED);
- const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
@@ -33,7 +27,6 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
// for archived issues list layout is the only option
const activeLayout = "list";
// hooks
- const { isMobile } = usePlatformOS();
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
@@ -68,60 +61,13 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
};
- const issueCount = currentProjectDetails
- ? issueFilters?.displayFilters?.sub_issue
- ? currentProjectDetails.archived_issues + currentProjectDetails.archived_sub_issues
- : currentProjectDetails.archived_issues
- : undefined;
-
return (
-
-
-
-
-
-
-
- )
- }
- />
- }
- />
-
- }
- />
- }
- />
-
- {issueCount && issueCount > 0 ? (
-
1 ? "issues" : "issue"} in project's archived`}
- position="bottom"
- >
-
- {issueCount}
-
-
- ) : null}
-
+
+
-
{/* filter options */}
-
+
= (props) => {
// hooks
const {
issue: { getIssueById },
+ peekIssue,
} = useIssueDetail();
// state
const [isCreateToggle, setIsCreateToggle] = useState(false);
@@ -82,13 +85,13 @@ export const LabelCreate: FC = (props) => {
{isCreateToggle && (
-