[WEB-3251] improvement: optimize projects API (#6542)
This commit is contained in:
parent
c14fb814c4
commit
10b5c625ef
52 changed files with 535 additions and 316 deletions
|
|
@ -90,16 +90,7 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
|
||||||
class ProjectListSerializer(DynamicBaseSerializer):
|
class ProjectListSerializer(DynamicBaseSerializer):
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
|
||||||
archived_issues = serializers.IntegerField(read_only=True)
|
|
||||||
archived_sub_issues = serializers.IntegerField(read_only=True)
|
|
||||||
draft_issues = serializers.IntegerField(read_only=True)
|
|
||||||
draft_sub_issues = serializers.IntegerField(read_only=True)
|
|
||||||
sub_issues = serializers.IntegerField(read_only=True)
|
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
|
||||||
total_cycles = serializers.IntegerField(read_only=True)
|
|
||||||
total_modules = serializers.IntegerField(read_only=True)
|
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
sort_order = serializers.FloatField(read_only=True)
|
sort_order = serializers.FloatField(read_only=True)
|
||||||
member_role = serializers.IntegerField(read_only=True)
|
member_role = serializers.IntegerField(read_only=True)
|
||||||
|
|
@ -113,14 +104,9 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||||
if project_members is not None:
|
if project_members is not None:
|
||||||
# Filter members by the project ID
|
# Filter members by the project ID
|
||||||
return [
|
return [
|
||||||
{
|
member.member_id
|
||||||
"id": member.id,
|
|
||||||
"member_id": member.member_id,
|
|
||||||
"member__display_name": member.member.display_name,
|
|
||||||
"member__avatar": member.member.avatar,
|
|
||||||
"member__avatar_url": member.member.avatar_url,
|
|
||||||
}
|
|
||||||
for member in project_members
|
for member in project_members
|
||||||
|
if member.is_active and not member.member.is_bot
|
||||||
]
|
]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -134,9 +120,6 @@ class ProjectDetailSerializer(BaseSerializer):
|
||||||
default_assignee = UserLiteSerializer(read_only=True)
|
default_assignee = UserLiteSerializer(read_only=True)
|
||||||
project_lead = UserLiteSerializer(read_only=True)
|
project_lead = UserLiteSerializer(read_only=True)
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
|
||||||
total_cycles = serializers.IntegerField(read_only=True)
|
|
||||||
total_modules = serializers.IntegerField(read_only=True)
|
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
sort_order = serializers.FloatField(read_only=True)
|
sort_order = serializers.FloatField(read_only=True)
|
||||||
member_role = serializers.IntegerField(read_only=True)
|
member_role = serializers.IntegerField(read_only=True)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from plane.app.views import (
|
||||||
SavedAnalyticEndpoint,
|
SavedAnalyticEndpoint,
|
||||||
ExportAnalyticsEndpoint,
|
ExportAnalyticsEndpoint,
|
||||||
DefaultAnalyticsEndpoint,
|
DefaultAnalyticsEndpoint,
|
||||||
|
ProjectStatsEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,4 +44,9 @@ urlpatterns = [
|
||||||
DefaultAnalyticsEndpoint.as_view(),
|
DefaultAnalyticsEndpoint.as_view(),
|
||||||
name="default-analytics",
|
name="default-analytics",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/project-stats/",
|
||||||
|
ProjectStatsEndpoint.as_view(),
|
||||||
|
name="project-analytics",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ urlpatterns = [
|
||||||
ProjectViewSet.as_view({"get": "list", "post": "create"}),
|
ProjectViewSet.as_view({"get": "list", "post": "create"}),
|
||||||
name="project",
|
name="project",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/details/",
|
||||||
|
ProjectViewSet.as_view({"get": "list_detail"}),
|
||||||
|
name="project",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||||
ProjectViewSet.as_view(
|
ProjectViewSet.as_view(
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ from .analytic.base import (
|
||||||
SavedAnalyticEndpoint,
|
SavedAnalyticEndpoint,
|
||||||
ExportAnalyticsEndpoint,
|
ExportAnalyticsEndpoint,
|
||||||
DefaultAnalyticsEndpoint,
|
DefaultAnalyticsEndpoint,
|
||||||
|
ProjectStatsEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .notification.base import (
|
from .notification.base import (
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from django.db.models import Count, F, Sum, Q
|
||||||
from django.db.models.functions import ExtractMonth
|
from django.db.models.functions import ExtractMonth
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
from django.db.models import Case, When, Value
|
from django.db.models import Case, When, Value, OuterRef, Func
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
|
@ -15,7 +15,16 @@ from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
from plane.app.serializers import AnalyticViewSerializer
|
from plane.app.serializers import AnalyticViewSerializer
|
||||||
from plane.app.views.base import BaseAPIView, BaseViewSet
|
from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||||
from plane.bgtasks.analytic_plot_export import analytic_export_task
|
from plane.bgtasks.analytic_plot_export import analytic_export_task
|
||||||
from plane.db.models import AnalyticView, Issue, Workspace
|
from plane.db.models import (
|
||||||
|
AnalyticView,
|
||||||
|
Issue,
|
||||||
|
Workspace,
|
||||||
|
Project,
|
||||||
|
ProjectMember,
|
||||||
|
Cycle,
|
||||||
|
Module,
|
||||||
|
)
|
||||||
|
|
||||||
from plane.utils.analytics_plot import build_graph_plot
|
from plane.utils.analytics_plot import build_graph_plot
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.app.permissions import allow_permission, ROLE
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
@ -441,3 +450,75 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectStatsEndpoint(BaseAPIView):
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||||
|
def get(self, request, slug):
|
||||||
|
fields = request.GET.get("fields", "").split(",")
|
||||||
|
project_ids = request.GET.get("project_ids", "").split(",")
|
||||||
|
|
||||||
|
valid_fields = {
|
||||||
|
"total_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"total_members",
|
||||||
|
"total_cycles",
|
||||||
|
"total_modules",
|
||||||
|
}
|
||||||
|
requested_fields = set(filter(None, fields)) & valid_fields
|
||||||
|
|
||||||
|
if not requested_fields:
|
||||||
|
requested_fields = valid_fields
|
||||||
|
|
||||||
|
projects = Project.objects.filter(workspace__slug=slug)
|
||||||
|
|
||||||
|
if project_ids:
|
||||||
|
projects = projects.filter(id__in=project_ids)
|
||||||
|
|
||||||
|
annotations = {}
|
||||||
|
if "total_issues" in requested_fields:
|
||||||
|
annotations["total_issues"] = (
|
||||||
|
Issue.issue_objects.filter(project_id=OuterRef("pk"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
if "completed_issues" in requested_fields:
|
||||||
|
annotations["completed_issues"] = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
project_id=OuterRef("pk"), state__group="completed"
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
if "total_cycles" in requested_fields:
|
||||||
|
annotations["total_cycles"] = (
|
||||||
|
Cycle.objects.filter(project_id=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
if "total_modules" in requested_fields:
|
||||||
|
annotations["total_modules"] = (
|
||||||
|
Module.objects.filter(project_id=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
if "total_members" in requested_fields:
|
||||||
|
annotations["total_members"] = (
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
project_id=OuterRef("id"), member__is_bot=False, is_active=True
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
projects = projects.annotate(**annotations).values("id", *requested_fields)
|
||||||
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
|
|
@ -25,12 +25,9 @@ from plane.app.serializers import (
|
||||||
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
|
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
UserFavorite,
|
UserFavorite,
|
||||||
Cycle,
|
|
||||||
Intake,
|
Intake,
|
||||||
DeployBoard,
|
DeployBoard,
|
||||||
IssueUserProperty,
|
IssueUserProperty,
|
||||||
Issue,
|
|
||||||
Module,
|
|
||||||
Project,
|
Project,
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
|
|
@ -83,26 +80,6 @@ class ProjectViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
|
||||||
total_members=ProjectMember.objects.filter(
|
|
||||||
project_id=OuterRef("id"), member__is_bot=False, is_active=True
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
total_modules=Module.objects.filter(project_id=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
.annotate(
|
||||||
member_role=ProjectMember.objects.filter(
|
member_role=ProjectMember.objects.filter(
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
|
|
@ -133,7 +110,7 @@ class ProjectViewSet(BaseViewSet):
|
||||||
@allow_permission(
|
@allow_permission(
|
||||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||||
)
|
)
|
||||||
def list(self, request, slug):
|
def list_detail(self, request, slug):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||||
projects = self.get_queryset().order_by("sort_order", "name")
|
projects = self.get_queryset().order_by("sort_order", "name")
|
||||||
if WorkspaceMember.objects.filter(
|
if WorkspaceMember.objects.filter(
|
||||||
|
|
@ -170,6 +147,76 @@ class ProjectViewSet(BaseViewSet):
|
||||||
).data
|
).data
|
||||||
return Response(projects, status=status.HTTP_200_OK)
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||||
|
)
|
||||||
|
def list(self, request, slug):
|
||||||
|
sort_order = ProjectMember.objects.filter(
|
||||||
|
member=self.request.user,
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
).values("sort_order")
|
||||||
|
|
||||||
|
projects = (
|
||||||
|
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related(
|
||||||
|
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_member=Exists(
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
member=self.request.user,
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(inbox_view=F("intake_view"))
|
||||||
|
.annotate(sort_order=Subquery(sort_order))
|
||||||
|
.distinct()
|
||||||
|
).values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"identifier",
|
||||||
|
"sort_order",
|
||||||
|
"logo_props",
|
||||||
|
"is_member",
|
||||||
|
"archived_at",
|
||||||
|
"workspace",
|
||||||
|
"cycle_view",
|
||||||
|
"issue_views_view",
|
||||||
|
"module_view",
|
||||||
|
"page_view",
|
||||||
|
"inbox_view",
|
||||||
|
"project_lead",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
)
|
||||||
|
|
||||||
|
if WorkspaceMember.objects.filter(
|
||||||
|
member=request.user, workspace__slug=slug, is_active=True, role=5
|
||||||
|
).exists():
|
||||||
|
projects = projects.filter(
|
||||||
|
project_projectmember__member=self.request.user,
|
||||||
|
project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if WorkspaceMember.objects.filter(
|
||||||
|
member=request.user, workspace__slug=slug, is_active=True, role=15
|
||||||
|
).exists():
|
||||||
|
projects = projects.filter(
|
||||||
|
Q(
|
||||||
|
project_projectmember__member=self.request.user,
|
||||||
|
project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
| Q(network=2)
|
||||||
|
)
|
||||||
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@allow_permission(
|
@allow_permission(
|
||||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||||
)
|
)
|
||||||
|
|
@ -182,58 +229,6 @@ class ProjectViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
.filter(archived_at__isnull=True)
|
.filter(archived_at__isnull=True)
|
||||||
.filter(pk=pk)
|
.filter(pk=pk)
|
||||||
.annotate(
|
|
||||||
total_issues=Issue.issue_objects.filter(
|
|
||||||
project_id=self.kwargs.get("pk")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
sub_issues=Issue.issue_objects.filter(
|
|
||||||
project_id=self.kwargs.get("pk"), parent__isnull=False
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
archived_issues=Issue.objects.filter(
|
|
||||||
project_id=self.kwargs.get("pk"), archived_at__isnull=False
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
archived_sub_issues=Issue.objects.filter(
|
|
||||||
project_id=self.kwargs.get("pk"),
|
|
||||||
archived_at__isnull=False,
|
|
||||||
parent__isnull=False,
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
draft_issues=Issue.objects.filter(
|
|
||||||
project_id=self.kwargs.get("pk"), is_draft=True
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
draft_sub_issues=Issue.objects.filter(
|
|
||||||
project_id=self.kwargs.get("pk"),
|
|
||||||
is_draft=True,
|
|
||||||
parent__isnull=False,
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if project is None:
|
if project is None:
|
||||||
|
|
|
||||||
2
packages/types/src/common.d.ts
vendored
2
packages/types/src/common.d.ts
vendored
|
|
@ -24,3 +24,5 @@ export type TLogoProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||||
|
|
||||||
|
export type TFetchStatus = "partial" | "complete" | undefined;
|
||||||
|
|
|
||||||
95
packages/types/src/project/projects.d.ts
vendored
95
packages/types/src/project/projects.d.ts
vendored
|
|
@ -10,58 +10,65 @@ import type {
|
||||||
} from "..";
|
} from "..";
|
||||||
import { TUserPermissions } from "../enums";
|
import { TUserPermissions } from "../enums";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IPartialProject {
|
||||||
archive_in: number;
|
id: string;
|
||||||
|
name: string;
|
||||||
|
identifier: string;
|
||||||
|
sort_order: number | null;
|
||||||
|
logo_props: TLogoProps;
|
||||||
|
is_member: boolean;
|
||||||
archived_at: string | null;
|
archived_at: string | null;
|
||||||
archived_issues: number;
|
workspace: IWorkspace | string;
|
||||||
archived_sub_issues: number;
|
|
||||||
completed_issues: number;
|
|
||||||
close_in: number;
|
|
||||||
created_at: Date;
|
|
||||||
created_by: string;
|
|
||||||
// only for uploading the cover image
|
|
||||||
cover_image_asset?: null;
|
|
||||||
cover_image?: string;
|
|
||||||
// only for rendering the cover image
|
|
||||||
cover_image_url: readonly string;
|
|
||||||
cycle_view: boolean;
|
cycle_view: boolean;
|
||||||
issue_views_view: boolean;
|
issue_views_view: boolean;
|
||||||
module_view: boolean;
|
module_view: boolean;
|
||||||
page_view: boolean;
|
page_view: boolean;
|
||||||
inbox_view: boolean;
|
inbox_view: boolean;
|
||||||
default_assignee: IUser | string | null;
|
project_lead?: IUserLite | string | null;
|
||||||
default_state: string | null;
|
// Timestamps
|
||||||
description: string;
|
created_at?: Date;
|
||||||
draft_issues: number;
|
updated_at?: Date;
|
||||||
draft_sub_issues: number;
|
// actor
|
||||||
estimate: string | null;
|
created_by?: string;
|
||||||
guest_view_all_features: boolean;
|
updated_by?: string;
|
||||||
id: string;
|
|
||||||
identifier: string;
|
|
||||||
anchor: string | null;
|
|
||||||
is_favorite: boolean;
|
|
||||||
is_issue_type_enabled: boolean;
|
|
||||||
is_member: boolean;
|
|
||||||
is_time_tracking_enabled: boolean;
|
|
||||||
logo_props: TLogoProps;
|
|
||||||
member_role: TUserPermissions | null;
|
|
||||||
members: IProjectMemberLite[];
|
|
||||||
name: string;
|
|
||||||
network: number;
|
|
||||||
project_lead: IUserLite | string | null;
|
|
||||||
sort_order: number | null;
|
|
||||||
sub_issues: number;
|
|
||||||
total_cycles: number;
|
|
||||||
total_issues: number;
|
|
||||||
total_members: number;
|
|
||||||
total_modules: number;
|
|
||||||
updated_at: Date;
|
|
||||||
updated_by: string;
|
|
||||||
workspace: IWorkspace | string;
|
|
||||||
workspace_detail: IWorkspaceLite;
|
|
||||||
timezone: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProject extends IPartialProject {
|
||||||
|
archive_in?: number;
|
||||||
|
close_in?: number;
|
||||||
|
// only for uploading the cover image
|
||||||
|
cover_image_asset?: null;
|
||||||
|
cover_image?: string;
|
||||||
|
// only for rendering the cover image
|
||||||
|
readonly cover_image_url?: string;
|
||||||
|
default_assignee?: IUser | string | null;
|
||||||
|
default_state?: string | null;
|
||||||
|
description?: string;
|
||||||
|
estimate?: string | null;
|
||||||
|
guest_view_all_features?: boolean;
|
||||||
|
anchor?: string | null;
|
||||||
|
is_favorite?: boolean;
|
||||||
|
is_issue_type_enabled?: boolean;
|
||||||
|
is_time_tracking_enabled?: boolean;
|
||||||
|
member_role?: TUserPermissions | null;
|
||||||
|
members?: string[];
|
||||||
|
network?: number;
|
||||||
|
timezone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TProjectAnalyticsCountParams = {
|
||||||
|
project_ids?: string;
|
||||||
|
fields?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectAnalyticsCount = Pick<IProject, "id"> & {
|
||||||
|
total_issues?: number;
|
||||||
|
completed_issues?: number;
|
||||||
|
total_cycles?: number;
|
||||||
|
total_members?: number;
|
||||||
|
total_modules?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IProjectLite {
|
export interface IProjectLite {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const AnalyticsPage = observer(() => {
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
{workspaceProjectIds && (
|
{workspaceProjectIds && (
|
||||||
<>
|
<>
|
||||||
{workspaceProjectIds.length > 0 || loader ? (
|
{workspaceProjectIds.length > 0 || loader === "init-loader" ? (
|
||||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
||||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||||
<Header variant={EHeaderVariant.SECONDARY}>
|
<Header variant={EHeaderVariant.SECONDARY}>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={
|
link={
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const CyclesListHeader: FC = observer(() => {
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// plane web
|
// plane web
|
||||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||||
|
|
||||||
|
// FIXME: Deprecated. Remove it
|
||||||
export const ProjectDraftIssueHeader: FC = observer(() => {
|
export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
||||||
|
|
@ -83,17 +84,13 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||||
[workspaceSlug, projectId, updateFilters]
|
[workspaceSlug, projectId, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
const issueCount = currentProjectDetails
|
const issueCount = undefined;
|
||||||
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails.draft_sub_issues
|
|
||||||
? currentProjectDetails.draft_issues - currentProjectDetails.draft_sub_issues
|
|
||||||
: currentProjectDetails.draft_issues
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
|
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const ProjectIssueDetailsHeader = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
|
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||||
/>
|
/>
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={
|
link={
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
const { loader } = useProject();
|
const { loader } = useProject();
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
const canUserCreateModule = allowPermissions(
|
const canUserCreateModule = allowPermissions(
|
||||||
|
|
@ -34,8 +34,8 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />}
|
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export const PageDetailsHeader = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={
|
link={
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const PagesListHeader = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const ProjectSettingHeader: FC = observer(() => {
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<div className="z-50">
|
<div className="z-50">
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<div className="hidden sm:hidden md:block lg:block">
|
<div className="hidden sm:hidden md:block lg:block">
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export const ProjectViewsHeader = observer(() => {
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export const IssuesHeader = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs onBack={() => router.back()} isLoading={loader}>
|
<Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
|
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const ProjectAttributes: FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 h-7" tabIndex={getIndex("lead")}>
|
<div className="flex-shrink-0 h-7" tabIndex={getIndex("lead")}>
|
||||||
<MemberDropdown
|
<MemberDropdown
|
||||||
value={value}
|
value={value ?? null}
|
||||||
onChange={(lead) => onChange(lead === value ? null : lead)}
|
onChange={(lead) => onChange(lead === value ? null : lead)}
|
||||||
placeholder={t("lead")}
|
placeholder={t("lead")}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Breadcrumbs isLoading={currentProjectDetailsLoader}>
|
<Breadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
|
||||||
<ProjectBreadcrumb />
|
<ProjectBreadcrumb />
|
||||||
|
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ type BlockData = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
sort_order: number | null;
|
sort_order: number | null;
|
||||||
start_date: string | undefined | null;
|
start_date?: string | undefined | null;
|
||||||
target_date: string | undefined | null;
|
target_date?: string | undefined | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IBaseTimelineStore {
|
export interface IBaseTimelineStore {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
import { IProject } from "@plane/types";
|
import { IPartialProject, IProject } from "@plane/types";
|
||||||
|
|
||||||
export type TProject = IProject;
|
export type TPartialProject = IPartialProject;
|
||||||
|
|
||||||
|
export type TProject = TPartialProject & IProject;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// icons
|
// icons
|
||||||
import { Contrast, LayoutGrid, Users } from "lucide-react";
|
import { Contrast, LayoutGrid, Users, Loader as Spinner } from "lucide-react";
|
||||||
|
// plane imports
|
||||||
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { Logo } from "@/components/common";
|
import { Logo } from "@/components/common";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -10,19 +12,25 @@ import { useProject } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectIds: string[];
|
projectIds: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
isUpdating: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((props) => {
|
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((props) => {
|
||||||
const { projectIds } = props;
|
const { projectIds, isLoading, isUpdating } = props;
|
||||||
|
// store hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById, getProjectAnalyticsCountById } = useProject();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col gap-4 h-full">
|
<div className="relative flex flex-col gap-4 h-full">
|
||||||
<h4 className="font-medium">Selected Projects</h4>
|
<div className="flex gap-2 items-center">
|
||||||
|
<h4 className="font-medium">Selected Projects</h4>
|
||||||
|
{isUpdating && <Spinner className="animate-spin size-3" />}
|
||||||
|
</div>
|
||||||
<div className="relative space-y-6 overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-md">
|
<div className="relative space-y-6 overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-md">
|
||||||
{projectIds.map((projectId) => {
|
{projectIds.map((projectId) => {
|
||||||
const project = getProjectById(projectId);
|
const project = getProjectById(projectId);
|
||||||
|
const projectAnalyticsCount = getProjectAnalyticsCountById(projectId);
|
||||||
|
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
|
|
@ -38,27 +46,37 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 w-full space-y-3 px-2">
|
<div className="mt-4 w-full space-y-3 px-2">
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
{isLoading ? (
|
||||||
<div className="flex items-center gap-2">
|
<Loader className="space-y-3">
|
||||||
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
|
<Loader.Item height="16px" />
|
||||||
<h6>Total members</h6>
|
<Loader.Item height="16px" />
|
||||||
</div>
|
<Loader.Item height="16px" />
|
||||||
<span className="text-custom-text-200">{project.total_members}</span>
|
</Loader>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
|
<div className="flex items-center gap-2">
|
||||||
<h6>Total cycles</h6>
|
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
</div>
|
<h6>Total members</h6>
|
||||||
<span className="text-custom-text-200">{project.total_cycles}</span>
|
</div>
|
||||||
</div>
|
<span className="text-custom-text-200">{projectAnalyticsCount?.total_members}</span>
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
|
<div className="flex items-center gap-2">
|
||||||
<h6>Total modules</h6>
|
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
</div>
|
<h6>Total cycles</h6>
|
||||||
<span className="text-custom-text-200">{project.total_modules}</span>
|
</div>
|
||||||
</div>
|
<span className="text-custom-text-200">{projectAnalyticsCount?.total_cycles}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
|
<h6>Total modules</h6>
|
||||||
|
</div>
|
||||||
|
<span className="text-custom-text-200">{projectAnalyticsCount?.total_modules}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// icons
|
// icons
|
||||||
import { CalendarDays, Download, RefreshCw } from "lucide-react";
|
import { CalendarDays, Download, RefreshCw } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
|
|
@ -30,19 +30,27 @@ type Props = {
|
||||||
|
|
||||||
const analyticsService = new AnalyticsService();
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
|
const PROJECT_ANALYTICS_COUNT_PARAMS = {
|
||||||
|
fields: "total_members,total_cycles,total_modules",
|
||||||
|
};
|
||||||
|
|
||||||
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { analytics, params, isProjectLevel = false } = props;
|
const { analytics, params, isProjectLevel = false } = props;
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = useParams();
|
const { workspaceSlug, projectId, cycleId, moduleId } = useParams();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { workspaceProjectIds, getProjectById } = useProject();
|
const { workspaceProjectIds, getProjectById, fetchProjectAnalyticsCount } = useProject();
|
||||||
const { getWorkspaceById } = useWorkspace();
|
const { getWorkspaceById } = useWorkspace();
|
||||||
|
|
||||||
const { fetchCycleDetails, getCycleById } = useCycle();
|
const { fetchCycleDetails, getCycleById } = useCycle();
|
||||||
const { fetchModuleDetails, getModuleById } = useModule();
|
const { fetchModuleDetails, getModuleById } = useModule();
|
||||||
|
// fetch project analytics count
|
||||||
|
const { isLoading: isProjectAnalyticsLoading, isValidating: isProjectAnalyticsUpdating } = useSWR(
|
||||||
|
workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null,
|
||||||
|
workspaceSlug ? () => fetchProjectAnalyticsCount(workspaceSlug.toString(), PROJECT_ANALYTICS_COUNT_PARAMS) : null
|
||||||
|
);
|
||||||
|
|
||||||
const projectDetails = projectId ? getProjectById(projectId.toString()) ?? undefined : undefined;
|
const projectDetails = projectId ? (getProjectById(projectId.toString()) ?? undefined) : undefined;
|
||||||
|
|
||||||
const trackExportAnalytics = () => {
|
const trackExportAnalytics = () => {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
|
|
@ -171,7 +179,11 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||||
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
|
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
|
||||||
<>
|
<>
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||||
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
<CustomAnalyticsSidebarProjectsList
|
||||||
|
projectIds={selectedProjects}
|
||||||
|
isLoading={isProjectAnalyticsLoading}
|
||||||
|
isUpdating={isProjectAnalyticsUpdating}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<CustomAnalyticsSidebarHeader />
|
<CustomAnalyticsSidebarHeader />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const JoinProject: React.FC = () => {
|
||||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { joinProject } = useUserPermissions();
|
const { joinProject } = useUserPermissions();
|
||||||
const { fetchProjects } = useProject();
|
const { fetchProjectDetails } = useProject();
|
||||||
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ export const JoinProject: React.FC = () => {
|
||||||
setIsJoiningProject(true);
|
setIsJoiningProject(true);
|
||||||
|
|
||||||
joinProject(workspaceSlug.toString(), projectId.toString())
|
joinProject(workspaceSlug.toString(), projectId.toString())
|
||||||
.then(() => fetchProjects(workspaceSlug.toString()))
|
.then(() => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()))
|
||||||
.finally(() => setIsJoiningProject(false));
|
.finally(() => setIsJoiningProject(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { ArchiveRestore } from "lucide-react";
|
import { ArchiveRestore } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
|
|
@ -23,6 +24,8 @@ const initialValues: Partial<IProject> = { archive_in: 1 };
|
||||||
|
|
||||||
export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
||||||
const { handleChange } = props;
|
const { handleChange } = props;
|
||||||
|
// router
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
// states
|
// states
|
||||||
const [monthModal, setmonthModal] = useState(false);
|
const [monthModal, setmonthModal] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -33,9 +36,9 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
EUserPermissionsLevel.PROJECT,
|
EUserPermissionsLevel.PROJECT,
|
||||||
currentProjectDetails?.workspace_detail?.slug,
|
workspaceSlug?.toString(),
|
||||||
currentProjectDetails?.id
|
currentProjectDetails?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
// icons
|
// icons
|
||||||
import { ArchiveX } from "lucide-react";
|
import { ArchiveX } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
|
|
@ -22,6 +23,8 @@ type Props = {
|
||||||
|
|
||||||
export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||||
const { handleChange } = props;
|
const { handleChange } = props;
|
||||||
|
// router
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
// states
|
// states
|
||||||
const [monthModal, setmonthModal] = useState(false);
|
const [monthModal, setmonthModal] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -59,7 +62,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
EUserPermissionsLevel.PROJECT,
|
EUserPermissionsLevel.PROJECT,
|
||||||
currentProjectDetails?.workspace_detail?.slug,
|
workspaceSlug?.toString(),
|
||||||
currentProjectDetails?.id
|
currentProjectDetails?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export const SelectMonthModal: React.FC<Props> = ({ type, initialValues, isOpen,
|
||||||
id="close_in"
|
id="close_in"
|
||||||
name="close_in"
|
name="close_in"
|
||||||
type="number"
|
type="number"
|
||||||
value={value.toString()}
|
value={value?.toString()}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.close_in)}
|
hasError={Boolean(errors.close_in)}
|
||||||
|
|
@ -127,7 +127,7 @@ export const SelectMonthModal: React.FC<Props> = ({ type, initialValues, isOpen,
|
||||||
id="archive_in"
|
id="archive_in"
|
||||||
name="archive_in"
|
name="archive_in"
|
||||||
type="number"
|
type="number"
|
||||||
value={value.toString()}
|
value={value?.toString()}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.archive_in)}
|
hasError={Boolean(errors.archive_in)}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,14 @@ import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard";
|
||||||
// helpers
|
// helpers
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useDashboard, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store";
|
import {
|
||||||
|
useEventTracker,
|
||||||
|
useDashboard,
|
||||||
|
useProject,
|
||||||
|
useCommandPalette,
|
||||||
|
useUserPermissions,
|
||||||
|
useMember,
|
||||||
|
} from "@/hooks/store";
|
||||||
// plane web constants
|
// plane web constants
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||||
|
|
||||||
|
|
@ -31,6 +38,8 @@ const ProjectListItem: React.FC<ProjectListItemProps> = observer((props) => {
|
||||||
const { projectId, workspaceSlug } = props;
|
const { projectId, workspaceSlug } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
// derived values
|
||||||
const projectDetails = getProjectById(projectId);
|
const projectDetails = getProjectById(projectId);
|
||||||
|
|
||||||
const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)];
|
const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)];
|
||||||
|
|
@ -52,13 +61,13 @@ const ProjectListItem: React.FC<ProjectListItemProps> = observer((props) => {
|
||||||
</h6>
|
</h6>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<AvatarGroup>
|
<AvatarGroup>
|
||||||
{projectDetails.members?.map((member) => (
|
{projectDetails.members?.map((memberId) => {
|
||||||
<Avatar
|
const userDetails = getUserDetails(memberId);
|
||||||
key={member.member_id}
|
if (!userDetails) return null;
|
||||||
src={getFileURL(member.member__avatar_url)}
|
return (
|
||||||
name={member.member__display_name}
|
<Avatar key={userDetails.id} src={getFileURL(userDetails.avatar_url)} name={userDetails.display_name} />
|
||||||
/>
|
);
|
||||||
))}
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export const RecentActivityWidget: React.FC<TRecentWidgetProps> = observer((prop
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!loader && !isWikiApp && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;
|
if (loader === "loaded" && !isWikiApp && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;
|
||||||
|
|
||||||
if (!isLoading && recents?.length === 0)
|
if (!isLoading && recents?.length === 0)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,11 @@ import useSize from "@/hooks/use-window-size";
|
||||||
|
|
||||||
export const WorkspaceDashboardView = observer(() => {
|
export const WorkspaceDashboardView = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const { captureEvent, setTrackElement } = useEventTracker();
|
||||||
// captureEvent,
|
|
||||||
setTrackElement,
|
|
||||||
} = useEventTracker();
|
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { toggleCreateProjectModal } = useCommandPalette();
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
|
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
|
||||||
const { captureEvent } = useEventTracker();
|
|
||||||
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
|
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
|
||||||
const { joinedProjectIds, loader } = useProject();
|
const { joinedProjectIds, loader } = useProject();
|
||||||
|
|
||||||
|
|
@ -63,7 +59,7 @@ export const WorkspaceDashboardView = observer(() => {
|
||||||
)}
|
)}
|
||||||
{homeDashboardId && joinedProjectIds && (
|
{homeDashboardId && joinedProjectIds && (
|
||||||
<>
|
<>
|
||||||
{joinedProjectIds.length > 0 || loader ? (
|
{joinedProjectIds.length > 0 || loader === "init-loader" ? (
|
||||||
<>
|
<>
|
||||||
<IssuePeekOverview />
|
<IssuePeekOverview />
|
||||||
<ContentWrapper
|
<ContentWrapper
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,19 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => {
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { toggleCreateProjectModal } = useCommandPalette();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
|
loader,
|
||||||
|
fetchStatus,
|
||||||
workspaceProjectIds: storeWorkspaceProjectIds,
|
workspaceProjectIds: storeWorkspaceProjectIds,
|
||||||
filteredProjectIds: storeFilteredProjectIds,
|
filteredProjectIds: storeFilteredProjectIds,
|
||||||
getProjectById,
|
getProjectById,
|
||||||
loader,
|
|
||||||
} = useProject();
|
} = useProject();
|
||||||
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
|
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
|
||||||
// derived values
|
// derived values
|
||||||
const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds;
|
const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds;
|
||||||
const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds;
|
const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds;
|
||||||
|
|
||||||
if (!filteredProjectIds || !workspaceProjectIds || loader) return <ProjectsLoader />;
|
if (!filteredProjectIds || !workspaceProjectIds || loader === "init-loader" || fetchStatus !== "complete")
|
||||||
|
return <ProjectsLoader />;
|
||||||
|
|
||||||
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
|
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
import { useMember, useProject, useUserPermissions } from "@/hooks/store";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// plane-web constants
|
// plane-web constants
|
||||||
|
|
@ -51,12 +51,13 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
// store hooks
|
// store hooks
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
const { addProjectToFavorites, removeProjectFromFavorites } = useProject();
|
const { addProjectToFavorites, removeProjectFromFavorites } = useProject();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
// hooks
|
// hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
// derived values
|
// derived values
|
||||||
const projectMembersIds = project.members?.map((member) => member.member_id);
|
const projectMembersIds = project.members;
|
||||||
const shouldRenderFavorite = allowPermissions(
|
const shouldRenderFavorite = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
EUserPermissionsLevel.WORKSPACE
|
EUserPermissionsLevel.WORKSPACE
|
||||||
|
|
@ -249,7 +250,7 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
||||||
if (project.is_favorite) handleRemoveFromFavorites();
|
if (project.is_favorite) handleRemoveFromFavorites();
|
||||||
else handleAddToFavorites();
|
else handleAddToFavorites();
|
||||||
}}
|
}}
|
||||||
selected={project.is_favorite}
|
selected={!!project.is_favorite}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -281,14 +282,10 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
||||||
<div className="flex cursor-pointer items-center gap-2 text-custom-text-200">
|
<div className="flex cursor-pointer items-center gap-2 text-custom-text-200">
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{projectMembersIds.map((memberId) => {
|
{projectMembersIds.map((memberId) => {
|
||||||
const member = project.members?.find((m) => m.member_id === memberId);
|
const member = getUserDetails(memberId);
|
||||||
if (!member) return null;
|
if (!member) return null;
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar key={member.id} name={member.display_name} src={getFileURL(member.avatar_url)} />
|
||||||
key={member.id}
|
|
||||||
name={member.member__display_name}
|
|
||||||
src={getFileURL(member.member__avatar_url)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
|
||||||
label={t("change_cover")}
|
label={t("change_cover")}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
control={control}
|
control={control}
|
||||||
value={value}
|
value={value ?? null}
|
||||||
tabIndex={getIndex("cover_image")}
|
tabIndex={getIndex("cover_image")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||||
label="Change cover"
|
label="Change cover"
|
||||||
control={control}
|
control={control}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value ?? null}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
/>
|
/>
|
||||||
|
|
@ -263,7 +263,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Description</h4>
|
<h4 className="text-sm">Summary</h4>
|
||||||
<Controller
|
<Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -272,7 +272,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Enter project description"
|
placeholder="Enter project summary"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="min-h-[102px] text-sm font-medium"
|
className="min-h-[102px] text-sm font-medium"
|
||||||
hasError={Boolean(errors?.description)}
|
hasError={Boolean(errors?.description)}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
||||||
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
|
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { joinProject } = useUserPermissions();
|
const { joinProject } = useUserPermissions();
|
||||||
const { fetchProjects } = useProject();
|
const { fetchProjectDetails } = useProject();
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
||||||
joinProject(workspaceSlug, project.id)
|
joinProject(workspaceSlug, project.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
|
router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
|
||||||
fetchProjects(workspaceSlug);
|
fetchProjectDetails(workspaceSlug, project.id);
|
||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { leaveProject } = useUserPermissions();
|
const { leaveProject } = useUserPermissions();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { fetchProjects } = useProject();
|
const { fetchProjectDetails } = useProject();
|
||||||
const {
|
const {
|
||||||
project: { removeMemberFromProject },
|
project: { removeMemberFromProject },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
|
@ -45,7 +45,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Project settings members page",
|
element: "Project settings members page",
|
||||||
});
|
});
|
||||||
await fetchProjects(workspaceSlug.toString());
|
await fetchProjectDetails(workspaceSlug.toString(), projectId.toString());
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToast({
|
setToast({
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
EUserPermissionsLevel.PROJECT,
|
EUserPermissionsLevel.PROJECT,
|
||||||
currentProjectDetails?.workspace_detail?.slug,
|
workspaceSlug?.toString(),
|
||||||
currentProjectDetails?.id
|
currentProjectDetails?.id
|
||||||
);
|
);
|
||||||
// form info
|
// form info
|
||||||
|
|
@ -176,7 +176,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={currentProjectDetails?.guest_view_all_features}
|
value={!!currentProjectDetails?.guest_view_all_features}
|
||||||
onChange={() => toggleGuestViewAllIssues(!currentProjectDetails?.guest_view_all_features)}
|
onChange={() => toggleGuestViewAllIssues(!currentProjectDetails?.guest_view_all_features)}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
size="md"
|
size="md"
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { sidebarCollapsed: isSidebarCollapsed, toggleSidebar } = useAppTheme();
|
const { sidebarCollapsed: isSidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||||
const { getProjectById } = useProject();
|
const { getPartialProjectById } = useProject();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
// pathname
|
// pathname
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// derived values
|
// derived values
|
||||||
const project = getProjectById(projectId);
|
const project = getPartialProjectById(projectId);
|
||||||
// handlers
|
// handlers
|
||||||
const handleProjectClick = () => {
|
const handleProjectClick = () => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
const { addProjectToFavorites, removeProjectFromFavorites, getPartialProjectById } = useProject();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
// states
|
// states
|
||||||
|
|
@ -70,7 +70,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
||||||
// derived values
|
// derived values
|
||||||
const project = getProjectById(projectId);
|
const project = getPartialProjectById(projectId);
|
||||||
// auth
|
// auth
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
|
|
@ -337,7 +337,8 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
useCaptureForOutsideClick
|
useCaptureForOutsideClick
|
||||||
>
|
>
|
||||||
{isAuthorized && (
|
{/* TODO: Removed is_favorite logic due to the optimization in projects API */}
|
||||||
|
{/* {isAuthorized && (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
|
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
|
||||||
>
|
>
|
||||||
|
|
@ -350,7 +351,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
<span>{project.is_favorite ? t("remove_from_favorites") : t("add_to_favorites")}</span>
|
<span>{project.is_favorite ? t("remove_from_favorites") : t("add_to_favorites")}</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{/* publish project settings */}
|
{/* publish project settings */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
|
@ -359,20 +360,10 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
|
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
|
||||||
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
|
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
</div>
|
</div>
|
||||||
<div>{project.anchor ? t("publish_settings") : t("publish")}</div>
|
<div>{t("publish_settings")}</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
{/* {isAuthorized && (
|
|
||||||
<CustomMenu.MenuItem>
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
|
|
||||||
<div className="flex items-center justify-start gap-2">
|
|
||||||
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
|
|
||||||
<span>Draft issues</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)} */}
|
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Briefcase, ChevronRight, Plus } from "lucide-react";
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// ui
|
// ui
|
||||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
import { Loader, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { CreateProjectModal } from "@/components/project";
|
import { CreateProjectModal } from "@/components/project";
|
||||||
import { SidebarProjectsListItem } from "@/components/workspace";
|
import { SidebarProjectsListItem } from "@/components/workspace";
|
||||||
|
|
@ -41,7 +41,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
const { getProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
|
const { loader, getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
|
||||||
// router params
|
// router params
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
@ -72,7 +72,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
||||||
|
|
||||||
const joinedProjectsList: TProject[] = [];
|
const joinedProjectsList: TProject[] = [];
|
||||||
joinedProjects.map((projectId) => {
|
joinedProjects.map((projectId) => {
|
||||||
const projectDetails = getProjectById(projectId);
|
const projectDetails = getPartialProjectById(projectId);
|
||||||
if (projectDetails) joinedProjectsList.push(projectDetails);
|
if (projectDetails) joinedProjectsList.push(projectDetails);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -232,6 +232,13 @@ export const SidebarProjectsList: FC = observer(() => {
|
||||||
leaveFrom="transform scale-100 opacity-100"
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
leaveTo="transform scale-95 opacity-0"
|
leaveTo="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
|
{loader === "init-loader" && (
|
||||||
|
<Loader className="w-full space-y-1.5">
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<Loader.Item key={index} height="28px" />
|
||||||
|
))}
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
{isAllProjectsListOpen && (
|
{isAllProjectsListOpen && (
|
||||||
<Disclosure.Panel
|
<Disclosure.Panel
|
||||||
as="div"
|
as="div"
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||||
if (projectExists && projectId && hasPermissionToCurrentProject === false) return <JoinProject />;
|
if (projectExists && projectId && hasPermissionToCurrentProject === false) return <JoinProject />;
|
||||||
|
|
||||||
// check if the project info is not found.
|
// check if the project info is not found.
|
||||||
if (!loader && !projectExists && projectId && !!hasPermissionToCurrentProject === false)
|
if (loader === "loaded" && !projectExists && projectId && !!hasPermissionToCurrentProject === false)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-screen place-items-center bg-custom-background-100">
|
<div className="grid h-screen place-items-center bg-custom-background-100">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { signOut, data: currentUser } = useUser();
|
const { signOut, data: currentUser } = useUser();
|
||||||
const { fetchProjects } = useProject();
|
const { fetchPartialProjects } = useProject();
|
||||||
const { fetchFavorite } = useFavorite();
|
const { fetchFavorite } = useFavorite();
|
||||||
const {
|
const {
|
||||||
workspace: { fetchWorkspaceMembers },
|
workspace: { fetchWorkspaceMembers },
|
||||||
|
|
@ -74,8 +74,8 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||||
|
|
||||||
// fetching workspace projects
|
// fetching workspace projects
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
|
workspaceSlug && currentWorkspace ? `WORKSPACE_PARTIAL_PROJECTS_${workspaceSlug}` : null,
|
||||||
workspaceSlug && currentWorkspace ? () => fetchProjects(workspaceSlug.toString()) : null,
|
workspaceSlug && currentWorkspace ? () => fetchPartialProjects(workspaceSlug.toString()) : null,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
// fetch workspace members
|
// fetch workspace members
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import type { GithubRepositoriesResponse, ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
import type {
|
||||||
|
GithubRepositoriesResponse,
|
||||||
|
ISearchIssueResponse,
|
||||||
|
TProjectAnalyticsCount,
|
||||||
|
TProjectAnalyticsCountParams,
|
||||||
|
TProjectIssuesSearchParams,
|
||||||
|
} from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
// plane web types
|
// plane web types
|
||||||
import { TProject } from "@/plane-web/types";
|
import { TProject, TPartialProject } from "@/plane-web/types";
|
||||||
// services
|
// services
|
||||||
import { APIService } from "@/services/api.service";
|
import { APIService } from "@/services/api.service";
|
||||||
|
|
||||||
|
|
@ -31,7 +37,7 @@ export class ProjectService extends APIService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(workspaceSlug: string): Promise<TProject[]> {
|
async getProjectsLite(workspaceSlug: string): Promise<TPartialProject[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
@ -39,6 +45,14 @@ export class ProjectService extends APIService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjects(workspaceSlug: string): Promise<TProject[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/details/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getProject(workspaceSlug: string, projectId: string): Promise<TProject> {
|
async getProject(workspaceSlug: string, projectId: string): Promise<TProject> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
|
@ -47,6 +61,19 @@ export class ProjectService extends APIService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectAnalyticsCount(
|
||||||
|
workspaceSlug: string,
|
||||||
|
params?: TProjectAnalyticsCountParams
|
||||||
|
): Promise<TProjectAnalyticsCount[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/project-stats/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateProject(workspaceSlug: string, projectId: string, data: Partial<TProject>): Promise<TProject> {
|
async updateProject(workspaceSlug: string, projectId: string, data: Partial<TProject>): Promise<TProject> {
|
||||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
|
@ -63,14 +90,6 @@ export class ProjectService extends APIService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchProjectEpicProperties(workspaceSlug: string, projectId: string): Promise<any> {
|
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epic-properties/`)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProjectView(
|
async setProjectView(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
|
import uniq from "lodash/uniq";
|
||||||
|
import update from "lodash/update";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
import {
|
import { IProjectBulkAddFormData, IProjectMember, IProjectMembership, IUserLite } from "@plane/types";
|
||||||
IProjectBulkAddFormData,
|
|
||||||
IProjectMember,
|
|
||||||
IProjectMemberLite,
|
|
||||||
IProjectMembership,
|
|
||||||
IUserLite,
|
|
||||||
} from "@plane/types";
|
|
||||||
// plane-web constants
|
// plane-web constants
|
||||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||||
// services
|
// services
|
||||||
|
|
@ -166,8 +162,11 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
set(this.projectMemberMap, [projectId, member.member], member);
|
set(this.projectMemberMap, [projectId, member.member], member);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members.concat(
|
update(this.projectRoot.projectMap, [projectId, "members"], (memberIds) =>
|
||||||
data.members as unknown as IProjectMemberLite[]
|
uniq([...memberIds, ...data.members.map((m) => m.member_id)])
|
||||||
|
);
|
||||||
|
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members?.concat(
|
||||||
|
data.members.map((m) => m.member_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -218,8 +217,8 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
delete this.projectMemberMap?.[projectId]?.[userId];
|
delete this.projectMemberMap?.[projectId]?.[userId];
|
||||||
});
|
});
|
||||||
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members.filter(
|
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members?.filter(
|
||||||
(member) => member.id !== userId
|
(memberId) => memberId !== userId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
|
import update from "lodash/update";
|
||||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
|
// plane imports
|
||||||
|
import { TFetchStatus, TLoader, TProjectAnalyticsCount, TProjectAnalyticsCountParams } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { orderProjects, shouldFilterProject } from "@/helpers/project.helper";
|
import { orderProjects, shouldFilterProject } from "@/helpers/project.helper";
|
||||||
// services
|
// services
|
||||||
import { TProject } from "@/plane-web/types/projects";
|
import { TProject, TPartialProject } from "@/plane-web/types/projects";
|
||||||
import { IssueLabelService, IssueService } from "@/services/issue";
|
import { IssueLabelService, IssueService } from "@/services/issue";
|
||||||
import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/services/project";
|
import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/services/project";
|
||||||
// store
|
// store
|
||||||
|
|
@ -16,10 +20,10 @@ type ProjectOverviewCollapsible = "links" | "attachments";
|
||||||
export interface IProjectStore {
|
export interface IProjectStore {
|
||||||
// observables
|
// observables
|
||||||
isUpdatingProject: boolean;
|
isUpdatingProject: boolean;
|
||||||
loader: boolean;
|
loader: TLoader;
|
||||||
projectMap: {
|
fetchStatus: TFetchStatus;
|
||||||
[projectId: string]: TProject; // projectId: project Info
|
projectMap: Record<string, TProject>; // projectId: project info
|
||||||
};
|
projectAnalyticsCountMap: Record<string, TProjectAnalyticsCount>; // projectId: project analytics count
|
||||||
// computed
|
// computed
|
||||||
filteredProjectIds: string[] | undefined;
|
filteredProjectIds: string[] | undefined;
|
||||||
workspaceProjectIds: string[] | undefined;
|
workspaceProjectIds: string[] | undefined;
|
||||||
|
|
@ -30,7 +34,9 @@ export interface IProjectStore {
|
||||||
currentProjectDetails: TProject | undefined;
|
currentProjectDetails: TProject | undefined;
|
||||||
// actions
|
// actions
|
||||||
getProjectById: (projectId: string | undefined | null) => TProject | undefined;
|
getProjectById: (projectId: string | undefined | null) => TProject | undefined;
|
||||||
|
getPartialProjectById: (projectId: string | undefined | null) => TPartialProject | undefined;
|
||||||
getProjectIdentifierById: (projectId: string | undefined | null) => string;
|
getProjectIdentifierById: (projectId: string | undefined | null) => string;
|
||||||
|
getProjectAnalyticsCountById: (projectId: string | undefined | null) => TProjectAnalyticsCount | undefined;
|
||||||
// collapsible
|
// collapsible
|
||||||
openCollapsibleSection: ProjectOverviewCollapsible[];
|
openCollapsibleSection: ProjectOverviewCollapsible[];
|
||||||
lastCollapsibleAction: ProjectOverviewCollapsible | null;
|
lastCollapsibleAction: ProjectOverviewCollapsible | null;
|
||||||
|
|
@ -40,8 +46,13 @@ export interface IProjectStore {
|
||||||
toggleOpenCollapsibleSection: (section: ProjectOverviewCollapsible) => void;
|
toggleOpenCollapsibleSection: (section: ProjectOverviewCollapsible) => void;
|
||||||
|
|
||||||
// fetch actions
|
// fetch actions
|
||||||
|
fetchPartialProjects: (workspaceSlug: string) => Promise<TPartialProject[]>;
|
||||||
fetchProjects: (workspaceSlug: string) => Promise<TProject[]>;
|
fetchProjects: (workspaceSlug: string) => Promise<TProject[]>;
|
||||||
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<TProject>;
|
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<TProject>;
|
||||||
|
fetchProjectAnalyticsCount: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
params?: TProjectAnalyticsCountParams
|
||||||
|
) => Promise<TProjectAnalyticsCount[]>;
|
||||||
// favorites actions
|
// favorites actions
|
||||||
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
|
|
@ -59,10 +70,10 @@ export interface IProjectStore {
|
||||||
export class ProjectStore implements IProjectStore {
|
export class ProjectStore implements IProjectStore {
|
||||||
// observables
|
// observables
|
||||||
isUpdatingProject: boolean = false;
|
isUpdatingProject: boolean = false;
|
||||||
loader: boolean = false;
|
loader: TLoader = "init-loader";
|
||||||
projectMap: {
|
fetchStatus: TFetchStatus = undefined;
|
||||||
[projectId: string]: TProject; // projectId: project Info
|
projectMap: Record<string, TProject> = {};
|
||||||
} = {};
|
projectAnalyticsCountMap: Record<string, TProjectAnalyticsCount> = {};
|
||||||
openCollapsibleSection: ProjectOverviewCollapsible[] = [];
|
openCollapsibleSection: ProjectOverviewCollapsible[] = [];
|
||||||
lastCollapsibleAction: ProjectOverviewCollapsible | null = null;
|
lastCollapsibleAction: ProjectOverviewCollapsible | null = null;
|
||||||
|
|
||||||
|
|
@ -80,7 +91,9 @@ export class ProjectStore implements IProjectStore {
|
||||||
// observables
|
// observables
|
||||||
isUpdatingProject: observable,
|
isUpdatingProject: observable,
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
|
fetchStatus: observable.ref,
|
||||||
projectMap: observable,
|
projectMap: observable,
|
||||||
|
projectAnalyticsCountMap: observable,
|
||||||
openCollapsibleSection: observable.ref,
|
openCollapsibleSection: observable.ref,
|
||||||
lastCollapsibleAction: observable.ref,
|
lastCollapsibleAction: observable.ref,
|
||||||
// computed
|
// computed
|
||||||
|
|
@ -92,8 +105,10 @@ export class ProjectStore implements IProjectStore {
|
||||||
joinedProjectIds: computed,
|
joinedProjectIds: computed,
|
||||||
favoriteProjectIds: computed,
|
favoriteProjectIds: computed,
|
||||||
// fetch actions
|
// fetch actions
|
||||||
|
fetchPartialProjects: action,
|
||||||
fetchProjects: action,
|
fetchProjects: action,
|
||||||
fetchProjectDetails: action,
|
fetchProjectDetails: action,
|
||||||
|
fetchProjectAnalyticsCount: action,
|
||||||
// favorites actions
|
// favorites actions
|
||||||
addProjectToFavorites: action,
|
addProjectToFavorites: action,
|
||||||
removeProjectFromFavorites: action,
|
removeProjectFromFavorites: action,
|
||||||
|
|
@ -241,6 +256,31 @@ export class ProjectStore implements IProjectStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get Workspace projects partial data using workspace slug
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @returns Promise<TPartialProject[]>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fetchPartialProjects = async (workspaceSlug: string) => {
|
||||||
|
try {
|
||||||
|
this.loader = "init-loader";
|
||||||
|
const projectsResponse = await this.projectService.getProjectsLite(workspaceSlug);
|
||||||
|
runInAction(() => {
|
||||||
|
projectsResponse.forEach((project) => {
|
||||||
|
update(this.projectMap, [project.id], (p) => ({ ...p, ...project }));
|
||||||
|
});
|
||||||
|
this.loader = "loaded";
|
||||||
|
this.fetchStatus = "partial";
|
||||||
|
});
|
||||||
|
return projectsResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Failed to fetch project from workspace store");
|
||||||
|
this.loader = "loaded";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get Workspace projects using workspace slug
|
* get Workspace projects using workspace slug
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
@ -249,18 +289,23 @@ export class ProjectStore implements IProjectStore {
|
||||||
*/
|
*/
|
||||||
fetchProjects = async (workspaceSlug: string) => {
|
fetchProjects = async (workspaceSlug: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
if (this.workspaceProjectIds && this.workspaceProjectIds.length > 0) {
|
||||||
|
this.loader = "mutation";
|
||||||
|
} else {
|
||||||
|
this.loader = "init-loader";
|
||||||
|
}
|
||||||
const projectsResponse = await this.projectService.getProjects(workspaceSlug);
|
const projectsResponse = await this.projectService.getProjects(workspaceSlug);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
projectsResponse.forEach((project) => {
|
projectsResponse.forEach((project) => {
|
||||||
set(this.projectMap, [project.id], project);
|
update(this.projectMap, [project.id], (p) => ({ ...p, ...project }));
|
||||||
});
|
});
|
||||||
this.loader = false;
|
this.loader = "loaded";
|
||||||
|
this.fetchStatus = "complete";
|
||||||
});
|
});
|
||||||
return projectsResponse;
|
return projectsResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to fetch project from workspace store");
|
console.log("Failed to fetch project from workspace store");
|
||||||
this.loader = false;
|
this.loader = "loaded";
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -275,7 +320,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
try {
|
try {
|
||||||
const response = await this.projectService.getProject(workspaceSlug, projectId);
|
const response = await this.projectService.getProject(workspaceSlug, projectId);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.projectMap, [projectId], response);
|
update(this.projectMap, [projectId], (p) => ({ ...p, ...response }));
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -284,6 +329,30 @@ export class ProjectStore implements IProjectStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches project analytics count using workspace slug and project id
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param params TProjectAnalyticsCountParams
|
||||||
|
* @returns Promise<TProjectAnalyticsCount[]>
|
||||||
|
*/
|
||||||
|
fetchProjectAnalyticsCount = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
params?: TProjectAnalyticsCountParams
|
||||||
|
): Promise<TProjectAnalyticsCount[]> => {
|
||||||
|
try {
|
||||||
|
const response = await this.projectService.getProjectAnalyticsCount(workspaceSlug, params);
|
||||||
|
runInAction(() => {
|
||||||
|
for (const analyticsData of response) {
|
||||||
|
set(this.projectAnalyticsCountMap, [analyticsData.id], analyticsData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Failed to fetch project analytics count", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns project details using project id
|
* Returns project details using project id
|
||||||
* @param projectId
|
* @param projectId
|
||||||
|
|
@ -294,6 +363,17 @@ export class ProjectStore implements IProjectStore {
|
||||||
return projectInfo;
|
return projectInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns project lite using project id
|
||||||
|
* This method is used just for type safety
|
||||||
|
* @param projectId
|
||||||
|
* @returns TPartialProject | null
|
||||||
|
*/
|
||||||
|
getPartialProjectById = computedFn((projectId: string | undefined | null) => {
|
||||||
|
const projectInfo = this.projectMap[projectId ?? ""] || undefined;
|
||||||
|
return projectInfo;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns project identifier using project id
|
* Returns project identifier using project id
|
||||||
* @param projectId
|
* @param projectId
|
||||||
|
|
@ -304,6 +384,16 @@ export class ProjectStore implements IProjectStore {
|
||||||
return projectInfo?.identifier;
|
return projectInfo?.identifier;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns project analytics count using project id
|
||||||
|
* @param projectId
|
||||||
|
* @returns TProjectAnalyticsCount[]
|
||||||
|
*/
|
||||||
|
getProjectAnalyticsCountById = computedFn((projectId: string | undefined | null) => {
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
return this.projectAnalyticsCountMap?.[projectId];
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds project to favorites and updates project favorite status in the store
|
* Adds project to favorites and updates project favorite status in the store
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
@ -415,8 +505,8 @@ export class ProjectStore implements IProjectStore {
|
||||||
* @returns Promise<TProject>
|
* @returns Promise<TProject>
|
||||||
*/
|
*/
|
||||||
updateProject = async (workspaceSlug: string, projectId: string, data: Partial<TProject>) => {
|
updateProject = async (workspaceSlug: string, projectId: string, data: Partial<TProject>) => {
|
||||||
|
const projectDetails = cloneDeep(this.getProjectById(projectId));
|
||||||
try {
|
try {
|
||||||
const projectDetails = this.getProjectById(projectId);
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.projectMap, [projectId], { ...projectDetails, ...data });
|
set(this.projectMap, [projectId], { ...projectDetails, ...data });
|
||||||
this.isUpdatingProject = true;
|
this.isUpdatingProject = true;
|
||||||
|
|
@ -428,9 +518,8 @@ export class ProjectStore implements IProjectStore {
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to create project from project store");
|
console.log("Failed to create project from project store");
|
||||||
this.fetchProjects(workspaceSlug);
|
|
||||||
this.fetchProjectDetails(workspaceSlug, projectId);
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
set(this.projectMap, [projectId], projectDetails);
|
||||||
this.isUpdatingProject = false;
|
this.isUpdatingProject = false;
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -454,7 +543,6 @@ export class ProjectStore implements IProjectStore {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to delete project from project store");
|
console.log("Failed to delete project from project store");
|
||||||
this.fetchProjects(workspaceSlug);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -476,8 +564,6 @@ export class ProjectStore implements IProjectStore {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log("Failed to archive project from project store");
|
console.log("Failed to archive project from project store");
|
||||||
this.fetchProjects(workspaceSlug);
|
|
||||||
this.fetchProjectDetails(workspaceSlug, projectId);
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -498,8 +584,6 @@ export class ProjectStore implements IProjectStore {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log("Failed to restore project from project store");
|
console.log("Failed to restore project from project store");
|
||||||
this.fetchProjects(workspaceSlug);
|
|
||||||
this.fetchProjectDetails(workspaceSlug, projectId);
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ export const shouldFilterProject = (
|
||||||
if (filterKey === "lead" && filters.lead && filters.lead.length > 0)
|
if (filterKey === "lead" && filters.lead && filters.lead.length > 0)
|
||||||
fallsInFilters = fallsInFilters && filters.lead.includes(`${project.project_lead}`);
|
fallsInFilters = fallsInFilters && filters.lead.includes(`${project.project_lead}`);
|
||||||
if (filterKey === "members" && filters.members && filters.members.length > 0) {
|
if (filterKey === "members" && filters.members && filters.members.length > 0) {
|
||||||
const memberIds = project.members.map((member) => member.member_id);
|
const memberIds = project.members;
|
||||||
fallsInFilters = fallsInFilters && filters.members.some((memberId) => memberIds.includes(memberId));
|
fallsInFilters = fallsInFilters && filters.members.some((memberId) => memberIds?.includes(memberId));
|
||||||
}
|
}
|
||||||
if (filterKey === "created_at" && filters.created_at && filters.created_at.length > 0) {
|
if (filterKey === "created_at" && filters.created_at && filters.created_at.length > 0) {
|
||||||
const createdDate = getDate(project.created_at);
|
const createdDate = getDate(project.created_at);
|
||||||
|
|
@ -109,8 +109,8 @@ export const orderProjects = (projects: TProject[], orderByKey: TProjectOrderByO
|
||||||
if (orderByKey === "-name") orderedProjects = sortBy(projects, [(p) => p.name.toLowerCase()]).reverse();
|
if (orderByKey === "-name") orderedProjects = sortBy(projects, [(p) => p.name.toLowerCase()]).reverse();
|
||||||
if (orderByKey === "created_at") orderedProjects = sortBy(projects, [(p) => p.created_at]);
|
if (orderByKey === "created_at") orderedProjects = sortBy(projects, [(p) => p.created_at]);
|
||||||
if (orderByKey === "-created_at") orderedProjects = sortBy(projects, [(p) => !p.created_at]);
|
if (orderByKey === "-created_at") orderedProjects = sortBy(projects, [(p) => !p.created_at]);
|
||||||
if (orderByKey === "members_length") orderedProjects = sortBy(projects, [(p) => p.members.length]);
|
if (orderByKey === "members_length") orderedProjects = sortBy(projects, [(p) => p.members?.length]);
|
||||||
if (orderByKey === "-members_length") orderedProjects = sortBy(projects, [(p) => p.members.length]).reverse();
|
if (orderByKey === "-members_length") orderedProjects = sortBy(projects, [(p) => p.members?.length]).reverse();
|
||||||
|
|
||||||
return orderedProjects;
|
return orderedProjects;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue