forked from github/plane
[WEB - 466] perf: improve performance for cycle and module endpoints (#3711)
* dev: improve performance for cycle apis * dev: reduce module endpoints and create a new endpoint for getting issues by list * dev: remove unwanted fields from module * dev: update module endpoints * dev: optimize cycle endpoints * change module and cycle types * dev: module optimizations * dev: fix the issues check * dev: fix issues endpoint * dev: update module detail serializer * modify adding issues to modules and cycles * dev: update cycle issues * fix module links * dev: optimize issue list endpoint * fix: removing issues from the module when removing module_id from issue peekoverview * fix: updated the tooltip and ui for cycle select (#3718) * fix: updated the tooltip and ui for module select (#3716) --------- Co-authored-by: rahulramesha <rahulramesham@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
parent
92becbc617
commit
ab3c3a6cf9
@ -72,6 +72,7 @@ from .issue import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module import (
|
||||||
|
ModuleDetailSerializer,
|
||||||
ModuleWriteSerializer,
|
ModuleWriteSerializer,
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
|
@ -3,10 +3,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .user import UserLiteSerializer
|
|
||||||
from .issue import IssueStateSerializer
|
from .issue import IssueStateSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
|
||||||
from .project import ProjectLiteSerializer
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
@ -14,7 +11,6 @@ from plane.db.models import (
|
|||||||
CycleUserProperties,
|
CycleUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CycleWriteSerializer(BaseSerializer):
|
class CycleWriteSerializer(BaseSerializer):
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if (
|
if (
|
||||||
@ -30,60 +26,6 @@ class CycleWriteSerializer(BaseSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class CycleSerializer(BaseSerializer):
|
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
|
||||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
|
||||||
completed_issues = serializers.IntegerField(read_only=True)
|
|
||||||
started_issues = serializers.IntegerField(read_only=True)
|
|
||||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
|
||||||
backlog_issues = serializers.IntegerField(read_only=True)
|
|
||||||
assignees = serializers.SerializerMethodField(read_only=True)
|
|
||||||
total_estimates = serializers.IntegerField(read_only=True)
|
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
|
||||||
status = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
if (
|
|
||||||
data.get("start_date", None) is not None
|
|
||||||
and data.get("end_date", None) is not None
|
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
|
||||||
):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
"Start date cannot exceed end date"
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_assignees(self, obj):
|
|
||||||
members = [
|
|
||||||
{
|
|
||||||
"avatar": assignee.avatar,
|
|
||||||
"display_name": assignee.display_name,
|
|
||||||
"id": assignee.id,
|
|
||||||
}
|
|
||||||
for issue_cycle in obj.issue_cycle.prefetch_related(
|
|
||||||
"issue__assignees"
|
|
||||||
).all()
|
|
||||||
for assignee in issue_cycle.issue.assignees.all()
|
|
||||||
]
|
|
||||||
# Use a set comprehension to return only the unique objects
|
|
||||||
unique_objects = {frozenset(item.items()) for item in members}
|
|
||||||
|
|
||||||
# Convert the set back to a list of dictionaries
|
|
||||||
unique_list = [dict(item) for item in unique_objects]
|
|
||||||
|
|
||||||
return unique_list
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Cycle
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
@ -91,6 +33,52 @@ class CycleSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CycleSerializer(BaseSerializer):
|
||||||
|
# favorite
|
||||||
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
|
# state group wise distribution
|
||||||
|
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||||
|
completed_issues = serializers.IntegerField(read_only=True)
|
||||||
|
started_issues = serializers.IntegerField(read_only=True)
|
||||||
|
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||||
|
backlog_issues = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
# active | draft | upcoming | completed
|
||||||
|
status = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Cycle
|
||||||
|
fields = [
|
||||||
|
# necessary fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"owned_by_id",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
"progress_snapshot",
|
||||||
|
# meta fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"status",
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueSerializer(BaseSerializer):
|
class CycleIssueSerializer(BaseSerializer):
|
||||||
issue_detail = IssueStateSerializer(read_only=True, source="issue")
|
issue_detail = IssueStateSerializer(read_only=True, source="issue")
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -5,7 +5,6 @@ from rest_framework import serializers
|
|||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
|
||||||
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
User,
|
User,
|
||||||
@ -19,17 +18,18 @@ from plane.db.models import (
|
|||||||
|
|
||||||
|
|
||||||
class ModuleWriteSerializer(BaseSerializer):
|
class ModuleWriteSerializer(BaseSerializer):
|
||||||
members = serializers.ListField(
|
lead_id = serializers.PrimaryKeyRelatedField(
|
||||||
|
source="lead",
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
member_ids = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -44,7 +44,9 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data["members"] = [str(member.id) for member in instance.members.all()]
|
data["member_ids"] = [
|
||||||
|
str(member.id) for member in instance.members.all()
|
||||||
|
]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -59,12 +61,10 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
members = validated_data.pop("members", None)
|
members = validated_data.pop("member_ids", None)
|
||||||
|
|
||||||
project = self.context["project"]
|
project = self.context["project"]
|
||||||
|
|
||||||
module = Module.objects.create(**validated_data, project=project)
|
module = Module.objects.create(**validated_data, project=project)
|
||||||
|
|
||||||
if members is not None:
|
if members is not None:
|
||||||
ModuleMember.objects.bulk_create(
|
ModuleMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
@ -85,7 +85,7 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
members = validated_data.pop("members", None)
|
members = validated_data.pop("member_ids", None)
|
||||||
|
|
||||||
if members is not None:
|
if members is not None:
|
||||||
ModuleMember.objects.filter(module=instance).delete()
|
ModuleMember.objects.filter(module=instance).delete()
|
||||||
@ -142,7 +142,6 @@ class ModuleIssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleLinkSerializer(BaseSerializer):
|
class ModuleLinkSerializer(BaseSerializer):
|
||||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleLink
|
model = ModuleLink
|
||||||
@ -170,12 +169,9 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(DynamicBaseSerializer):
|
class ModuleSerializer(DynamicBaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
member_ids = serializers.ListField(
|
||||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
child=serializers.UUIDField(), required=False, allow_null=True
|
||||||
members_detail = UserLiteSerializer(
|
|
||||||
read_only=True, many=True, source="members"
|
|
||||||
)
|
)
|
||||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||||
@ -186,15 +182,46 @@ class ModuleSerializer(DynamicBaseSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
fields = "__all__"
|
fields = [
|
||||||
read_only_fields = [
|
# Required fields
|
||||||
"workspace",
|
"id",
|
||||||
"project",
|
"workspace_id",
|
||||||
"created_by",
|
"project_id",
|
||||||
"updated_by",
|
# Model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"description_text",
|
||||||
|
"description_html",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"status",
|
||||||
|
"lead_id",
|
||||||
|
"member_ids",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
# computed fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDetailSerializer(ModuleSerializer):
|
||||||
|
|
||||||
|
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
|
class Meta(ModuleSerializer.Meta):
|
||||||
|
fields = ModuleSerializer.Meta.fields + ['link_module']
|
||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteSerializer(BaseSerializer):
|
class ModuleFavoriteSerializer(BaseSerializer):
|
||||||
|
@ -2,6 +2,7 @@ from django.urls import path
|
|||||||
|
|
||||||
|
|
||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
|
IssueListEndpoint,
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
LabelViewSet,
|
LabelViewSet,
|
||||||
BulkCreateIssueLabelsEndpoint,
|
BulkCreateIssueLabelsEndpoint,
|
||||||
@ -25,6 +26,11 @@ from plane.app.views import (
|
|||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/list/",
|
||||||
|
IssueListEndpoint.as_view(),
|
||||||
|
name="project-issue",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
|
||||||
IssueViewSet.as_view(
|
IssueViewSet.as_view(
|
||||||
|
@ -67,6 +67,7 @@ from .cycle import (
|
|||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||||
from .issue import (
|
from .issue import (
|
||||||
|
IssueListEndpoint,
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
WorkSpaceIssuesEndpoint,
|
WorkSpaceIssuesEndpoint,
|
||||||
IssueActivityEndpoint,
|
IssueActivityEndpoint,
|
||||||
|
@ -20,7 +20,10 @@ from django.core import serializers
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models import Value, UUIDField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -33,7 +36,6 @@ from plane.app.serializers import (
|
|||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
CycleFavoriteSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
IssueStateSerializer,
|
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
CycleUserPropertiesSerializer,
|
CycleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
@ -51,7 +53,6 @@ from plane.db.models import (
|
|||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
Label,
|
Label,
|
||||||
CycleUserProperties,
|
CycleUserProperties,
|
||||||
IssueSubscriber,
|
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
@ -73,7 +74,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
subquery = CycleFavorite.objects.filter(
|
favorite_subquery = CycleFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
cycle_id=OuterRef("pk"),
|
cycle_id=OuterRef("pk"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
@ -85,10 +86,24 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
.select_related("project")
|
.select_related("project", "workspace", "owned_by")
|
||||||
.select_related("workspace")
|
.prefetch_related(
|
||||||
.select_related("owned_by")
|
Prefetch(
|
||||||
.annotate(is_favorite=Exists(subquery))
|
"issue_cycle__issue__assignees",
|
||||||
|
queryset=User.objects.only(
|
||||||
|
"avatar", "first_name", "id"
|
||||||
|
).distinct(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_cycle__issue__labels",
|
||||||
|
queryset=Label.objects.only(
|
||||||
|
"name", "color", "id"
|
||||||
|
).distinct(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(is_favorite=Exists(favorite_subquery))
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
total_issues=Count(
|
||||||
"issue_cycle",
|
"issue_cycle",
|
||||||
@ -148,29 +163,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
|
||||||
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
completed_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(
|
|
||||||
issue_cycle__issue__state__group="completed",
|
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
|
||||||
issue_cycle__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(
|
|
||||||
issue_cycle__issue__state__group="started",
|
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
|
||||||
issue_cycle__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
.annotate(
|
||||||
status=Case(
|
status=Case(
|
||||||
When(
|
When(
|
||||||
@ -190,20 +182,16 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.annotate(
|
||||||
Prefetch(
|
assignee_ids=Coalesce(
|
||||||
"issue_cycle__issue__assignees",
|
ArrayAgg(
|
||||||
queryset=User.objects.only(
|
"issue_cycle__issue__assignees__id",
|
||||||
"avatar", "first_name", "id"
|
distinct=True,
|
||||||
).distinct(),
|
filter=~Q(
|
||||||
)
|
issue_cycle__issue__assignees__id__isnull=True
|
||||||
)
|
),
|
||||||
.prefetch_related(
|
),
|
||||||
Prefetch(
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
"issue_cycle__issue__labels",
|
|
||||||
queryset=Label.objects.only(
|
|
||||||
"name", "color", "id"
|
|
||||||
).distinct(),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-is_favorite", "name")
|
.order_by("-is_favorite", "name")
|
||||||
@ -213,12 +201,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
fields = [
|
|
||||||
field
|
|
||||||
for field in request.GET.get("fields", "").split(",")
|
|
||||||
if field
|
|
||||||
]
|
|
||||||
|
|
||||||
|
# Update the order by
|
||||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||||
|
|
||||||
# Current Cycle
|
# Current Cycle
|
||||||
@ -228,9 +212,35 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
end_date__gte=timezone.now(),
|
end_date__gte=timezone.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = CycleSerializer(queryset, many=True).data
|
data = queryset.values(
|
||||||
|
# necessary fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"owned_by_id",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
"progress_snapshot",
|
||||||
|
# meta fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"assignee_ids",
|
||||||
|
"status",
|
||||||
|
)
|
||||||
|
|
||||||
if len(data):
|
if data:
|
||||||
assignee_distribution = (
|
assignee_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
issue_cycle__cycle_id=data[0]["id"],
|
issue_cycle__cycle_id=data[0]["id"],
|
||||||
@ -315,19 +325,45 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if data[0]["start_date"] and data[0]["end_date"]:
|
if data[0]["start_date"] and data[0]["end_date"]:
|
||||||
data[0]["distribution"][
|
data[0]["distribution"]["completion_chart"] = (
|
||||||
"completion_chart"
|
burndown_plot(
|
||||||
] = burndown_plot(
|
queryset=queryset.first(),
|
||||||
queryset=queryset.first(),
|
slug=slug,
|
||||||
slug=slug,
|
project_id=project_id,
|
||||||
project_id=project_id,
|
cycle_id=data[0]["id"],
|
||||||
cycle_id=data[0]["id"],
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
cycles = CycleSerializer(queryset, many=True).data
|
data = queryset.values(
|
||||||
return Response(cycles, status=status.HTTP_200_OK)
|
# necessary fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"owned_by_id",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
"progress_snapshot",
|
||||||
|
# meta fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"assignee_ids",
|
||||||
|
"status",
|
||||||
|
)
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
if (
|
if (
|
||||||
@ -337,7 +373,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
request.data.get("start_date", None) is not None
|
request.data.get("start_date", None) is not None
|
||||||
and request.data.get("end_date", None) is not None
|
and request.data.get("end_date", None) is not None
|
||||||
):
|
):
|
||||||
serializer = CycleSerializer(data=request.data)
|
serializer = CycleWriteSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(
|
serializer.save(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -346,12 +382,36 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
cycle = (
|
cycle = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
|
.values(
|
||||||
|
# necessary fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"owned_by_id",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
"progress_snapshot",
|
||||||
|
# meta fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"assignee_ids",
|
||||||
|
"status",
|
||||||
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
serializer = CycleSerializer(cycle)
|
return Response(cycle, status=status.HTTP_201_CREATED)
|
||||||
return Response(
|
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
@ -364,10 +424,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
cycle = Cycle.objects.get(
|
queryset = (
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
self.get_queryset()
|
||||||
|
.filter(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
)
|
)
|
||||||
|
cycle = queryset.first()
|
||||||
request_data = request.data
|
request_data = request.data
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -375,7 +436,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
and cycle.end_date < timezone.now().date()
|
and cycle.end_date < timezone.now().date()
|
||||||
):
|
):
|
||||||
if "sort_order" in request_data:
|
if "sort_order" in request_data:
|
||||||
# Can only change sort order
|
# Can only change sort order for a completed cycle``
|
||||||
request_data = {
|
request_data = {
|
||||||
"sort_order": request_data.get(
|
"sort_order": request_data.get(
|
||||||
"sort_order", cycle.sort_order
|
"sort_order", cycle.sort_order
|
||||||
@ -394,12 +455,71 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
cycle = queryset.values(
|
||||||
|
# necessary fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"owned_by_id",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
"progress_snapshot",
|
||||||
|
# meta fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"assignee_ids",
|
||||||
|
"status",
|
||||||
|
).first()
|
||||||
|
return Response(cycle, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
queryset = self.get_queryset().get(pk=pk)
|
queryset = self.get_queryset().filter(pk=pk)
|
||||||
|
data = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=pk)
|
||||||
|
.values(
|
||||||
|
# necessary fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"owned_by_id",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
"progress_snapshot",
|
||||||
|
# meta fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"assignee_ids",
|
||||||
|
"status",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
queryset = queryset.first()
|
||||||
# Assignee Distribution
|
# Assignee Distribution
|
||||||
assignee_distribution = (
|
assignee_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
@ -488,7 +608,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.order_by("label_name")
|
.order_by("label_name")
|
||||||
)
|
)
|
||||||
|
|
||||||
data = CycleSerializer(queryset).data
|
|
||||||
data["distribution"] = {
|
data["distribution"] = {
|
||||||
"assignees": assignee_distribution,
|
"assignees": assignee_distribution,
|
||||||
"labels": label_distribution,
|
"labels": label_distribution,
|
||||||
@ -591,20 +710,18 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.filter(project_id=project_id)
|
.filter(project_id=project_id)
|
||||||
.filter(workspace__slug=slug)
|
.filter(workspace__slug=slug)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
.prefetch_related(
|
||||||
|
"assignees",
|
||||||
|
"labels",
|
||||||
|
"issue_module__module",
|
||||||
|
"issue_cycle__cycle",
|
||||||
|
)
|
||||||
.order_by(order_by)
|
.order_by(order_by)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
|
.annotate(module_ids=F("issue_module__module_id"))
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
@ -621,11 +738,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_subscribed=Exists(
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
IssueSubscriber.objects.filter(
|
parent=OuterRef("id")
|
||||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
serializer = IssueSerializer(
|
serializer = IssueSerializer(
|
||||||
@ -636,7 +754,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
def create(self, request, slug, project_id, cycle_id):
|
def create(self, request, slug, project_id, cycle_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
|
|
||||||
if not len(issues):
|
if not issues:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"},
|
{"error": "Issues are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -658,52 +776,52 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Get all CycleIssues already created
|
# Get all CycleIssues already created
|
||||||
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
|
cycle_issues = list(
|
||||||
update_cycle_issue_activity = []
|
CycleIssue.objects.filter(
|
||||||
record_to_create = []
|
~Q(cycle_id=cycle_id), issue_id__in=issues
|
||||||
records_to_update = []
|
)
|
||||||
|
)
|
||||||
|
existing_issues = [
|
||||||
|
str(cycle_issue.issue_id) for cycle_issue in cycle_issues
|
||||||
|
]
|
||||||
|
new_issues = list(set(issues) - set(existing_issues))
|
||||||
|
|
||||||
for issue in issues:
|
# New issues to create
|
||||||
cycle_issue = [
|
created_records = CycleIssue.objects.bulk_create(
|
||||||
cycle_issue
|
[
|
||||||
for cycle_issue in cycle_issues
|
CycleIssue(
|
||||||
if str(cycle_issue.issue_id) in issues
|
project_id=project_id,
|
||||||
]
|
workspace_id=cycle.workspace_id,
|
||||||
# Update only when cycle changes
|
created_by_id=request.user.id,
|
||||||
if len(cycle_issue):
|
updated_by_id=request.user.id,
|
||||||
if cycle_issue[0].cycle_id != cycle_id:
|
cycle_id=cycle_id,
|
||||||
update_cycle_issue_activity.append(
|
issue_id=issue,
|
||||||
{
|
|
||||||
"old_cycle_id": str(cycle_issue[0].cycle_id),
|
|
||||||
"new_cycle_id": str(cycle_id),
|
|
||||||
"issue_id": str(cycle_issue[0].issue_id),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cycle_issue[0].cycle_id = cycle_id
|
|
||||||
records_to_update.append(cycle_issue[0])
|
|
||||||
else:
|
|
||||||
record_to_create.append(
|
|
||||||
CycleIssue(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace=cycle.workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
cycle=cycle,
|
|
||||||
issue_id=issue,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
for issue in new_issues
|
||||||
CycleIssue.objects.bulk_create(
|
],
|
||||||
record_to_create,
|
|
||||||
batch_size=10,
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
CycleIssue.objects.bulk_update(
|
|
||||||
records_to_update,
|
|
||||||
["cycle"],
|
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Updated Issues
|
||||||
|
updated_records = []
|
||||||
|
update_cycle_issue_activity = []
|
||||||
|
# Iterate over each cycle_issue in cycle_issues
|
||||||
|
for cycle_issue in cycle_issues:
|
||||||
|
# Update the cycle_issue's cycle_id
|
||||||
|
cycle_issue.cycle_id = cycle_id
|
||||||
|
# Add the modified cycle_issue to the records_to_update list
|
||||||
|
updated_records.append(cycle_issue)
|
||||||
|
# Record the update activity
|
||||||
|
update_cycle_issue_activity.append(
|
||||||
|
{
|
||||||
|
"old_cycle_id": str(cycle_issue.cycle_id),
|
||||||
|
"new_cycle_id": str(cycle_id),
|
||||||
|
"issue_id": str(cycle_issue.issue_id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the cycle issues
|
||||||
|
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
|
||||||
# Capture Issue Activity
|
# Capture Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.created",
|
type="cycle.activity.created",
|
||||||
@ -715,7 +833,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
{
|
{
|
||||||
"updated_cycle_issues": update_cycle_issue_activity,
|
"updated_cycle_issues": update_cycle_issue_activity,
|
||||||
"created_cycle_issues": serializers.serialize(
|
"created_cycle_issues": serializers.serialize(
|
||||||
"json", record_to_create
|
"json", created_records
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -723,16 +841,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
|
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||||
# Return all Cycle Issues
|
|
||||||
issues = self.get_queryset().values_list("issue_id", flat=True)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
IssueSerializer(
|
|
||||||
Issue.objects.filter(pk__in=issues), many=True
|
|
||||||
).data,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
cycle_issue = CycleIssue.objects.get(
|
cycle_issue = CycleIssue.objects.get(
|
||||||
@ -776,6 +885,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if any cycle intersects in the given interval
|
||||||
cycles = Cycle.objects.filter(
|
cycles = Cycle.objects.filter(
|
||||||
Q(workspace__slug=slug)
|
Q(workspace__slug=slug)
|
||||||
& Q(project_id=project_id)
|
& Q(project_id=project_id)
|
||||||
@ -785,7 +895,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
|||||||
| Q(start_date__gte=start_date, end_date__lte=end_date)
|
| Q(start_date__gte=start_date, end_date__lte=end_date)
|
||||||
)
|
)
|
||||||
).exclude(pk=cycle_id)
|
).exclude(pk=cycle_id)
|
||||||
|
|
||||||
if cycles.exists():
|
if cycles.exists():
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -909,29 +1018,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
|
||||||
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
completed_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(
|
|
||||||
issue_cycle__issue__state__group="completed",
|
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
|
||||||
issue_cycle__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
started_estimates=Sum(
|
|
||||||
"issue_cycle__issue__estimate_point",
|
|
||||||
filter=Q(
|
|
||||||
issue_cycle__issue__state__group="started",
|
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
|
||||||
issue_cycle__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pass the new_cycle queryset to burndown_plot
|
# Pass the new_cycle queryset to burndown_plot
|
||||||
@ -942,6 +1028,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
cycle_id=cycle_id,
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get the assignee distribution
|
||||||
assignee_distribution = (
|
assignee_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
issue_cycle__cycle_id=cycle_id,
|
issue_cycle__cycle_id=cycle_id,
|
||||||
@ -980,7 +1067,22 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.order_by("display_name")
|
.order_by("display_name")
|
||||||
)
|
)
|
||||||
|
# assignee distribution serialized
|
||||||
|
assignee_distribution_data = [
|
||||||
|
{
|
||||||
|
"display_name": item["display_name"],
|
||||||
|
"assignee_id": (
|
||||||
|
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||||
|
),
|
||||||
|
"avatar": item["avatar"],
|
||||||
|
"total_issues": item["total_issues"],
|
||||||
|
"completed_issues": item["completed_issues"],
|
||||||
|
"pending_issues": item["pending_issues"],
|
||||||
|
}
|
||||||
|
for item in assignee_distribution
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get the label distribution
|
||||||
label_distribution = (
|
label_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
issue_cycle__cycle_id=cycle_id,
|
issue_cycle__cycle_id=cycle_id,
|
||||||
@ -1019,24 +1121,14 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.order_by("label_name")
|
.order_by("label_name")
|
||||||
)
|
)
|
||||||
|
# Label distribution serilization
|
||||||
assignee_distribution_data = [
|
|
||||||
{
|
|
||||||
"display_name": item["display_name"],
|
|
||||||
"assignee_id": str(item["assignee_id"]) if item["assignee_id"] else None,
|
|
||||||
"avatar": item["avatar"],
|
|
||||||
"total_issues": item["total_issues"],
|
|
||||||
"completed_issues": item["completed_issues"],
|
|
||||||
"pending_issues": item["pending_issues"],
|
|
||||||
}
|
|
||||||
for item in assignee_distribution
|
|
||||||
]
|
|
||||||
|
|
||||||
label_distribution_data = [
|
label_distribution_data = [
|
||||||
{
|
{
|
||||||
"label_name": item["label_name"],
|
"label_name": item["label_name"],
|
||||||
"color": item["color"],
|
"color": item["color"],
|
||||||
"label_id": str(item["label_id"]) if item["label_id"] else None,
|
"label_id": (
|
||||||
|
str(item["label_id"]) if item["label_id"] else None
|
||||||
|
),
|
||||||
"total_issues": item["total_issues"],
|
"total_issues": item["total_issues"],
|
||||||
"completed_issues": item["completed_issues"],
|
"completed_issues": item["completed_issues"],
|
||||||
"pending_issues": item["pending_issues"],
|
"pending_issues": item["pending_issues"],
|
||||||
@ -1058,7 +1150,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
"total_estimates": old_cycle.first().total_estimates,
|
"total_estimates": old_cycle.first().total_estimates,
|
||||||
"completed_estimates": old_cycle.first().completed_estimates,
|
"completed_estimates": old_cycle.first().completed_estimates,
|
||||||
"started_estimates": old_cycle.first().started_estimates,
|
"started_estimates": old_cycle.first().started_estimates,
|
||||||
"distribution":{
|
"distribution": {
|
||||||
"labels": label_distribution_data,
|
"labels": label_distribution_data,
|
||||||
"assignees": assignee_distribution_data,
|
"assignees": assignee_distribution_data,
|
||||||
"completion_chart": completion_chart,
|
"completion_chart": completion_chart,
|
||||||
|
@ -4,7 +4,6 @@ import random
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Prefetch,
|
Prefetch,
|
||||||
@ -12,19 +11,21 @@ from django.db.models import (
|
|||||||
Func,
|
Func,
|
||||||
F,
|
F,
|
||||||
Q,
|
Q,
|
||||||
Count,
|
|
||||||
Case,
|
Case,
|
||||||
Value,
|
Value,
|
||||||
CharField,
|
CharField,
|
||||||
When,
|
When,
|
||||||
Exists,
|
Exists,
|
||||||
Max,
|
Max,
|
||||||
IntegerField,
|
|
||||||
)
|
)
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models import Value, UUIDField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -67,15 +68,11 @@ from plane.db.models import (
|
|||||||
Label,
|
Label,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
State,
|
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
ProjectDeployBoard,
|
|
||||||
IssueVote,
|
|
||||||
IssueRelation,
|
IssueRelation,
|
||||||
ProjectPublicMember,
|
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
@ -83,6 +80,192 @@ from plane.utils.issue_filters import issue_filters
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
class IssueListEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
issue_ids = request.GET.get("issues", False)
|
||||||
|
|
||||||
|
if not issue_ids:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_ids = [issue_id for issue_id in issue_ids.split(",") if issue_id != ""]
|
||||||
|
|
||||||
|
queryset = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = queryset.filter(**filters)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
if self.fields or self.expand:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
queryset, many=True, fields=self.fields, expand=self.expand
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = issue_queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(WebhookMixin, BaseViewSet):
|
class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
return (
|
return (
|
||||||
@ -1085,7 +1268,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -1132,10 +1315,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
issue_queryset = (
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
self.get_queryset()
|
|
||||||
.filter(**filters)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
@ -1580,15 +1760,17 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
issue_relation = IssueRelation.objects.bulk_create(
|
issue_relation = IssueRelation.objects.bulk_create(
|
||||||
[
|
[
|
||||||
IssueRelation(
|
IssueRelation(
|
||||||
issue_id=issue
|
issue_id=(
|
||||||
if relation_type == "blocking"
|
issue if relation_type == "blocking" else issue_id
|
||||||
else issue_id,
|
),
|
||||||
related_issue_id=issue_id
|
related_issue_id=(
|
||||||
if relation_type == "blocking"
|
issue_id if relation_type == "blocking" else issue
|
||||||
else issue,
|
),
|
||||||
relation_type="blocked_by"
|
relation_type=(
|
||||||
if relation_type == "blocking"
|
"blocked_by"
|
||||||
else relation_type,
|
if relation_type == "blocking"
|
||||||
|
else relation_type
|
||||||
|
),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
@ -1669,9 +1851,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
|
||||||
project_id=self.kwargs.get("project_id")
|
|
||||||
)
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(is_draft=True)
|
.filter(is_draft=True)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
@ -1728,10 +1908,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
issue_queryset = (
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
self.get_queryset()
|
|
||||||
.filter(**filters)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
@ -1830,7 +2007,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
issue = (
|
issue = (
|
||||||
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||||
)
|
)
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
IssueSerializer(issue).data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
|
@ -4,11 +4,12 @@ import json
|
|||||||
# Django Imports
|
# Django Imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||||
from django.core import serializers
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models import Value, UUIDField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -24,6 +25,7 @@ from plane.app.serializers import (
|
|||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
ModuleUserPropertiesSerializer,
|
ModuleUserPropertiesSerializer,
|
||||||
|
ModuleDetailSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@ -38,11 +40,9 @@ from plane.db.models import (
|
|||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
IssueSubscriber,
|
|
||||||
ModuleUserProperties,
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
subquery = ModuleFavorite.objects.filter(
|
favorite_subquery = ModuleFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
module_id=OuterRef("pk"),
|
module_id=OuterRef("pk"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
@ -73,7 +73,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.annotate(is_favorite=Exists(subquery))
|
.annotate(is_favorite=Exists(favorite_subquery))
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("lead")
|
.select_related("lead")
|
||||||
@ -145,6 +145,16 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
member_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"members__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(members__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
)
|
||||||
|
)
|
||||||
.order_by("-is_favorite", "-created_at")
|
.order_by("-is_favorite", "-created_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,25 +167,84 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
module = Module.objects.get(pk=serializer.data["id"])
|
module = (
|
||||||
serializer = ModuleSerializer(module)
|
self.get_queryset()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
.filter(pk=serializer.data["id"])
|
||||||
|
.values( # Required fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# Model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"description_text",
|
||||||
|
"description_html",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"status",
|
||||||
|
"lead_id",
|
||||||
|
"member_ids",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
# computed fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
return Response(module, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
fields = [
|
if self.fields:
|
||||||
field
|
modules = ModuleSerializer(
|
||||||
for field in request.GET.get("fields", "").split(",")
|
queryset,
|
||||||
if field
|
many=True,
|
||||||
]
|
fields=self.fields,
|
||||||
modules = ModuleSerializer(
|
).data
|
||||||
queryset, many=True, fields=fields if fields else None
|
else:
|
||||||
).data
|
modules = queryset.values( # Required fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# Model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"description_text",
|
||||||
|
"description_html",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"status",
|
||||||
|
"lead_id",
|
||||||
|
"member_ids",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
# computed fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
return Response(modules, status=status.HTTP_200_OK)
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
queryset = self.get_queryset().get(pk=pk)
|
queryset = self.get_queryset().filter(pk=pk)
|
||||||
|
|
||||||
assignee_distribution = (
|
assignee_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
@ -269,16 +338,16 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.order_by("label_name")
|
.order_by("label_name")
|
||||||
)
|
)
|
||||||
|
|
||||||
data = ModuleSerializer(queryset).data
|
data = ModuleDetailSerializer(queryset.first()).data
|
||||||
data["distribution"] = {
|
data["distribution"] = {
|
||||||
"assignees": assignee_distribution,
|
"assignees": assignee_distribution,
|
||||||
"labels": label_distribution,
|
"labels": label_distribution,
|
||||||
"completion_chart": {},
|
"completion_chart": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryset.start_date and queryset.target_date:
|
if queryset.first().start_date and queryset.first().target_date:
|
||||||
data["distribution"]["completion_chart"] = burndown_plot(
|
data["distribution"]["completion_chart"] = burndown_plot(
|
||||||
queryset=queryset,
|
queryset=queryset.first(),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
module_id=pk,
|
module_id=pk,
|
||||||
@ -289,6 +358,47 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
|
queryset = self.get_queryset().filter(pk=pk)
|
||||||
|
serializer = ModuleWriteSerializer(
|
||||||
|
queryset.first(), data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
module = queryset.values(
|
||||||
|
# Required fields
|
||||||
|
"id",
|
||||||
|
"workspace_id",
|
||||||
|
"project_id",
|
||||||
|
# Model fields
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"description_text",
|
||||||
|
"description_html",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"status",
|
||||||
|
"lead_id",
|
||||||
|
"member_ids",
|
||||||
|
"view_props",
|
||||||
|
"sort_order",
|
||||||
|
"external_source",
|
||||||
|
"external_id",
|
||||||
|
# computed fields
|
||||||
|
"is_favorite",
|
||||||
|
"total_issues",
|
||||||
|
"cancelled_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"started_issues",
|
||||||
|
"unstarted_issues",
|
||||||
|
"backlog_issues",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
).first()
|
||||||
|
return Response(module, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
@ -331,17 +441,16 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
issue_module__module_id=self.kwargs.get("module_id")
|
issue_module__module_id=self.kwargs.get("module_id"),
|
||||||
)
|
)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("labels", "assignees")
|
.prefetch_related("labels", "assignees")
|
||||||
.prefetch_related('issue_module__module')
|
.prefetch_related("issue_module__module")
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
@ -384,7 +493,7 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
# create multiple issues inside a module
|
# create multiple issues inside a module
|
||||||
def create_module_issues(self, request, slug, project_id, module_id):
|
def create_module_issues(self, request, slug, project_id, module_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
if not len(issues):
|
if not issues:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"},
|
{"error": "Issues are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -420,15 +529,12 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
for issue in issues
|
for issue in issues
|
||||||
]
|
]
|
||||||
issues = (self.get_queryset().filter(pk__in=issues))
|
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||||
serializer = IssueSerializer(issues , many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
|
|
||||||
# create multiple module inside an issue
|
# create multiple module inside an issue
|
||||||
def create_issue_modules(self, request, slug, project_id, issue_id):
|
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||||
modules = request.data.get("modules", [])
|
modules = request.data.get("modules", [])
|
||||||
if not len(modules):
|
if not modules:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Modules are required"},
|
{"error": "Modules are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -466,10 +572,7 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
for module in modules
|
for module in modules
|
||||||
]
|
]
|
||||||
|
|
||||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||||
serializer = IssueSerializer(issue)
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, module_id, issue_id):
|
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||||
module_issue = ModuleIssue.objects.get(
|
module_issue = ModuleIssue.objects.get(
|
||||||
@ -484,7 +587,9 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=json.dumps({"module_name": module_issue.module.name}),
|
current_instance=json.dumps(
|
||||||
|
{"module_name": module_issue.module.name}
|
||||||
|
),
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
8
packages/types/src/cycles.d.ts
vendored
8
packages/types/src/cycles.d.ts
vendored
@ -32,8 +32,7 @@ export interface ICycle {
|
|||||||
name: string;
|
name: string;
|
||||||
owned_by: string;
|
owned_by: string;
|
||||||
progress_snapshot: TProgressSnapshot;
|
progress_snapshot: TProgressSnapshot;
|
||||||
project: string;
|
project_id: string;
|
||||||
project_detail: IProjectLite;
|
|
||||||
status: TCycleGroups;
|
status: TCycleGroups;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
start_date: string | null;
|
start_date: string | null;
|
||||||
@ -42,12 +41,11 @@ export interface ICycle {
|
|||||||
unstarted_issues: number;
|
unstarted_issues: number;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
assignees: IUserLite[];
|
assignee_ids: string[];
|
||||||
view_props: {
|
view_props: {
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
workspace: string;
|
workspace_id: string;
|
||||||
workspace_detail: IWorkspaceLite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TProgressSnapshot = {
|
export type TProgressSnapshot = {
|
||||||
|
1
packages/types/src/issues.d.ts
vendored
1
packages/types/src/issues.d.ts
vendored
@ -58,7 +58,6 @@ export interface IIssueLink {
|
|||||||
export interface ILinkDetails {
|
export interface ILinkDetails {
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
created_by_detail: IUserLite;
|
|
||||||
id: string;
|
id: string;
|
||||||
metadata: any;
|
metadata: any;
|
||||||
title: string;
|
title: string;
|
||||||
|
13
packages/types/src/modules.d.ts
vendored
13
packages/types/src/modules.d.ts
vendored
@ -27,16 +27,12 @@ export interface IModule {
|
|||||||
labels: TLabelsDistribution[];
|
labels: TLabelsDistribution[];
|
||||||
};
|
};
|
||||||
id: string;
|
id: string;
|
||||||
lead: string | null;
|
lead_id: string | null;
|
||||||
lead_detail: IUserLite | null;
|
|
||||||
link_module: ILinkDetails[];
|
link_module: ILinkDetails[];
|
||||||
links_list: ModuleLink[];
|
member_ids: string[];
|
||||||
members: string[];
|
|
||||||
members_detail: IUserLite[];
|
|
||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
project: string;
|
project_id: string;
|
||||||
project_detail: IProjectLite;
|
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
start_date: string | null;
|
start_date: string | null;
|
||||||
started_issues: number;
|
started_issues: number;
|
||||||
@ -49,8 +45,7 @@ export interface IModule {
|
|||||||
view_props: {
|
view_props: {
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
workspace: string;
|
workspace_id: string;
|
||||||
workspace_detail: IWorkspaceLite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModuleIssueResponse {
|
export interface ModuleIssueResponse {
|
||||||
|
@ -21,6 +21,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||||
const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined;
|
const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
|
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
|
||||||
|
const moduleLeadDetails = moduleDetails && moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -57,7 +58,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-custom-text-200">Lead</h6>
|
<h6 className="text-custom-text-200">Lead</h6>
|
||||||
<span>{moduleDetails.lead_detail?.display_name}</span>
|
{moduleLeadDetails && <span>{moduleLeadDetails?.display_name}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<h6 className="text-custom-text-200">Start Date</h6>
|
<h6 className="text-custom-text-200">Start Date</h6>
|
||||||
|
@ -5,7 +5,7 @@ import { mutate } from "swr";
|
|||||||
// services
|
// services
|
||||||
import { AnalyticsService } from "services/analytics.service";
|
import { AnalyticsService } from "services/analytics.service";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle, useModule, useProject, useUser } from "hooks/store";
|
import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
|
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
|
||||||
@ -39,6 +39,8 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
const { workspaceProjectIds, getProjectById } = useProject();
|
const { workspaceProjectIds, getProjectById } = useProject();
|
||||||
|
const { getWorkspaceById } = useWorkspace();
|
||||||
|
|
||||||
const { fetchCycleDetails, getCycleById } = useCycle();
|
const { fetchCycleDetails, getCycleById } = useCycle();
|
||||||
const { fetchModuleDetails, getModuleById } = useModule();
|
const { fetchModuleDetails, getModuleById } = useModule();
|
||||||
|
|
||||||
@ -70,11 +72,14 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
if (cycleDetails || moduleDetails) {
|
if (cycleDetails || moduleDetails) {
|
||||||
const details = cycleDetails || moduleDetails;
|
const details = cycleDetails || moduleDetails;
|
||||||
|
|
||||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
const currentProjectDetails = getProjectById(details?.project_id || "");
|
||||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
const currentWorkspaceDetails = getWorkspaceById(details?.workspace_id || "");
|
||||||
eventPayload.projectId = details?.project_detail.id;
|
|
||||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
eventPayload.workspaceId = details?.workspace_id;
|
||||||
eventPayload.projectName = details?.project_detail.name;
|
eventPayload.workspaceName = currentWorkspaceDetails?.name;
|
||||||
|
eventPayload.projectId = details?.project_id;
|
||||||
|
eventPayload.projectIdentifier = currentProjectDetails?.identifier;
|
||||||
|
eventPayload.projectName = currentProjectDetails?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cycleDetails) {
|
if (cycleDetails) {
|
||||||
@ -138,14 +143,18 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100", !isProjectLevel ? "flex-col" : "")}
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100",
|
||||||
|
!isProjectLevel ? "flex-col" : ""
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||||
<LayersIcon height={14} width={14} />
|
<LayersIcon height={14} width={14} />
|
||||||
{analytics ? analytics.total : "..."} <div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
{analytics ? analytics.total : "..."}
|
||||||
|
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
||||||
</div>
|
</div>
|
||||||
{isProjectLevel && (
|
{isProjectLevel && (
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||||
@ -154,8 +163,8 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
(cycleId
|
(cycleId
|
||||||
? cycleDetails?.created_at
|
? cycleDetails?.created_at
|
||||||
: moduleId
|
: moduleId
|
||||||
? moduleDetails?.created_at
|
? moduleDetails?.created_at
|
||||||
: projectDetails?.created_at) ?? ""
|
: projectDetails?.created_at) ?? ""
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -8,6 +8,9 @@ import { calculateTimeAgo } from "helpers/date-time.helper";
|
|||||||
import { ILinkDetails, UserAuth } from "@plane/types";
|
import { ILinkDetails, UserAuth } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useMeasure } from "@nivo/core";
|
||||||
|
import { useMember } from "hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
links: ILinkDetails[];
|
links: ILinkDetails[];
|
||||||
@ -16,9 +19,10 @@ type Props = {
|
|||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
@ -33,70 +37,75 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{links.map((link) => (
|
{links.map((link) => {
|
||||||
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
const createdByDetails = getUserDetails(link.created_by);
|
||||||
<div className="flex w-full items-start justify-between gap-2">
|
return (
|
||||||
<div className="flex items-start gap-2 truncate">
|
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
|
||||||
<span className="py-1">
|
<div className="flex w-full items-start justify-between gap-2">
|
||||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
<div className="flex items-start gap-2 truncate">
|
||||||
</span>
|
<span className="py-1">
|
||||||
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}>
|
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
<span
|
|
||||||
className="cursor-pointer truncate text-xs"
|
|
||||||
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
|
|
||||||
>
|
|
||||||
{link.title && link.title !== "" ? link.title : link.url}
|
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}>
|
||||||
</div>
|
<span
|
||||||
|
className="cursor-pointer truncate text-xs"
|
||||||
{!isNotAllowed && (
|
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
|
||||||
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
|
>
|
||||||
<button
|
{link.title && link.title !== "" ? link.title : link.url}
|
||||||
type="button"
|
</span>
|
||||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
</Tooltip>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleEditLink(link);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
href={link.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
|
||||||
>
|
|
||||||
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleDeleteLink(link.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{!isNotAllowed && (
|
||||||
|
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEditLink(link);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteLink(link.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-5">
|
||||||
|
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||||
|
Added {calculateTimeAgo(link.created_at)}
|
||||||
|
<br />
|
||||||
|
{createdByDetails && (
|
||||||
|
<>
|
||||||
|
by{" "}
|
||||||
|
{createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5">
|
);
|
||||||
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
})}
|
||||||
Added {calculateTimeAgo(link.created_at)}
|
|
||||||
<br />
|
|
||||||
by{" "}
|
|
||||||
{link.created_by_detail.is_bot
|
|
||||||
? link.created_by_detail.first_name + " Bot"
|
|
||||||
: link.created_by_detail.display_name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -222,12 +222,13 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
|||||||
<span className="text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
|
<span className="text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeCycle.assignees.length > 0 && (
|
{activeCycle.assignee_ids.length > 0 && (
|
||||||
<div className="flex items-center gap-1 text-custom-text-200">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<AvatarGroup>
|
<AvatarGroup>
|
||||||
{activeCycle.assignees.map((assignee) => (
|
{activeCycle.assignee_ids.map((assigne_id) => {
|
||||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
const member = getUserDetails(assigne_id);
|
||||||
))}
|
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||||
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useCycle, useUser } from "hooks/store";
|
import { useEventTracker, useCycle, useUser, useMember } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||||
@ -40,6 +40,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
|
const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// computed
|
// computed
|
||||||
@ -212,13 +213,14 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
|||||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||||
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
||||||
</div>
|
</div>
|
||||||
{cycleDetails.assignees.length > 0 && (
|
{cycleDetails.assignee_ids.length > 0 && (
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids.length} Members`}>
|
||||||
<div className="flex cursor-default items-center gap-1">
|
<div className="flex cursor-default items-center gap-1">
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{cycleDetails.assignees.map((assignee) => (
|
{cycleDetails.assignee_ids.map((assigne_id) => {
|
||||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
const member = getUserDetails(assigne_id);
|
||||||
))}
|
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||||
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useCycle, useUser } from "hooks/store";
|
import { useEventTracker, useCycle, useUser, useMember } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||||
@ -44,6 +44,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
|
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -230,13 +231,14 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-shrink-0 items-center gap-3">
|
<div className="relative flex flex-shrink-0 items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center">
|
<div className="flex w-10 cursor-default items-center justify-center">
|
||||||
{cycleDetails.assignees.length > 0 ? (
|
{cycleDetails.assignee_ids?.length > 0 ? (
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{cycleDetails.assignees.map((assignee) => (
|
{cycleDetails.assignee_ids?.map((assigne_id) => {
|
||||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
|
const member = getUserDetails(assigne_id);
|
||||||
))}
|
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||||
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||||
|
@ -36,7 +36,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
reset,
|
reset,
|
||||||
} = useForm<ICycle>({
|
} = useForm<ICycle>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
project: projectId,
|
project_id: projectId,
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
description: data?.description || "",
|
description: data?.description || "",
|
||||||
start_date: data?.start_date || null,
|
start_date: data?.start_date || null,
|
||||||
@ -61,13 +61,13 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
maxDate?.setDate(maxDate.getDate() - 1);
|
maxDate?.setDate(maxDate.getDate() - 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit((formData)=>handleFormSubmit(formData,dirtyFields))}>
|
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
{!status && (
|
{!status && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="project"
|
name="project_id"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<ProjectDropdown
|
<ProjectDropdown
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -40,7 +40,7 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
? "rgb(var(--color-text-200))"
|
? "rgb(var(--color-text-200))"
|
||||||
: "",
|
: "",
|
||||||
}}
|
}}
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -78,7 +78,7 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center gap-2"
|
className="relative flex h-full w-full items-center gap-2"
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<ContrastIcon
|
<ContrastIcon
|
||||||
className="h-5 w-5 flex-shrink-0"
|
className="h-5 w-5 flex-shrink-0"
|
||||||
|
@ -33,7 +33,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
|||||||
const payload: any = { ...data };
|
const payload: any = { ...data };
|
||||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||||
|
|
||||||
await updateCycleDetails(workspaceSlug.toString(), cycle.project, cycle.id, payload);
|
await updateCycleDetails(workspaceSlug.toString(), cycle.project_id, cycle.id, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const blockFormat = (blocks: (ICycle | null)[]) => {
|
const blockFormat = (blocks: (ICycle | null)[]) => {
|
||||||
|
@ -40,7 +40,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
const handleCreateCycle = async (payload: Partial<ICycle>) => {
|
const handleCreateCycle = async (payload: Partial<ICycle>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project_id ?? projectId.toString();
|
||||||
await createCycle(workspaceSlug, selectedProjectId, payload)
|
await createCycle(workspaceSlug, selectedProjectId, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -69,7 +69,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
|
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project_id ?? projectId.toString();
|
||||||
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const changed_properties = Object.keys(dirtyFields);
|
const changed_properties = Object.keys(dirtyFields);
|
||||||
@ -155,8 +155,8 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
|
|
||||||
// if data is present, set active project to the project of the
|
// if data is present, set active project to the project of the
|
||||||
// issue. This has more priority than the project in the url.
|
// issue. This has more priority than the project in the url.
|
||||||
if (data && data.project) {
|
if (data && data.project_id) {
|
||||||
setActiveProject(data.project);
|
setActiveProject(data.project_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
|||||||
const filteredOptions = currentProjectIncompleteCycleIds?.filter((optionId) => {
|
const filteredOptions = currentProjectIncompleteCycleIds?.filter((optionId) => {
|
||||||
const cycleDetails = getCycleById(optionId);
|
const cycleDetails = getCycleById(optionId);
|
||||||
|
|
||||||
return cycleDetails?.name.toLowerCase().includes(query.toLowerCase());
|
return cycleDetails?.name?.toLowerCase().includes(query?.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
@ -10,11 +10,12 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
// components
|
// components
|
||||||
import { DropdownButton } from "./buttons";
|
import { DropdownButton } from "./buttons";
|
||||||
// icons
|
// icons
|
||||||
import { ContrastIcon } from "@plane/ui";
|
import { ContrastIcon, CycleGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
import { TCycleGroups } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
|
|
||||||
@ -82,17 +83,22 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle();
|
const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle();
|
||||||
const cycleIds = getProjectCycleIds(projectId);
|
|
||||||
|
const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => {
|
||||||
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
return cycleDetails?.status.toLowerCase() != "completed" ? true : false;
|
||||||
|
});
|
||||||
|
|
||||||
const options: DropdownOptions = cycleIds?.map((cycleId) => {
|
const options: DropdownOptions = cycleIds?.map((cycleId) => {
|
||||||
const cycleDetails = getCycleById(cycleId);
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
const cycleStatus = cycleDetails?.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: cycleId,
|
value: cycleId,
|
||||||
query: `${cycleDetails?.name}`,
|
query: `${cycleDetails?.name}`,
|
||||||
content: (
|
content: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ContrastIcon className="h-3 w-3 flex-shrink-0" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span className="flex-grow truncate">{cycleDetails?.name}</span>
|
<span className="flex-grow truncate">{cycleDetails?.name}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
@ -77,12 +77,16 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showCount ? (
|
{showCount ? (
|
||||||
<>
|
<div className="relative flex items-center gap-1">
|
||||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
<span className="flex-grow truncate text-left">
|
<div className="flex-grow truncate max-w-40">
|
||||||
{value.length > 0 ? `${value.length} Module${value.length === 1 ? "" : "s"}` : placeholder}
|
{value.length > 0
|
||||||
</span>
|
? value.length === 1
|
||||||
</>
|
? `${getModuleById(value[0])?.name || "module"}`
|
||||||
|
: `${value.length} Module${value.length === 1 ? "" : "s"}`
|
||||||
|
: placeholder}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : value.length > 0 ? (
|
) : value.length > 0 ? (
|
||||||
<div className="flex items-center gap-2 py-0.5 flex-wrap">
|
<div className="flex items-center gap-2 py-0.5 flex-wrap">
|
||||||
{value.map((moduleId) => {
|
{value.map((moduleId) => {
|
||||||
@ -298,7 +302,12 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
isActive={isOpen}
|
isActive={isOpen}
|
||||||
tooltipHeading="Module"
|
tooltipHeading="Module"
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
Array.isArray(value) ? `${value?.length ?? 0} module${value?.length !== 1 ? "s" : ""}` : ""
|
Array.isArray(value)
|
||||||
|
? `${value
|
||||||
|
.map((moduleId) => getModuleById(moduleId)?.name)
|
||||||
|
.toString()
|
||||||
|
.replaceAll(",", ", ")}`
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
showTooltip={showTooltip}
|
showTooltip={showTooltip}
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
|
@ -160,7 +160,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||||||
},
|
},
|
||||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
const response = await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Cycle added to issue successfully",
|
title: "Cycle added to issue successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -168,7 +168,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: ISSUE_UPDATED,
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
change_details: cycleId,
|
change_details: cycleId,
|
||||||
|
@ -60,19 +60,13 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const issueIds = data.map((i) => i.id);
|
const issueIds = data.map((i) => i.id);
|
||||||
|
|
||||||
await issues
|
await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => {
|
||||||
.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds)
|
setToastAlert({
|
||||||
.then((res) => {
|
type: "error",
|
||||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
title: "Error!",
|
||||||
fetchIssue(workspaceSlug, projectId, res.id);
|
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"];
|
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"];
|
||||||
|
@ -146,7 +146,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
},
|
},
|
||||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
const response = await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Cycle added to issue successfully",
|
title: "Cycle added to issue successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -154,7 +154,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: ISSUE_UPDATED,
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
change_details: cycleId,
|
change_details: cycleId,
|
||||||
|
@ -45,7 +45,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
await deleteModule(workspaceSlug.toString(), projectId.toString(), data.id)
|
await deleteModule(workspaceSlug.toString(), projectId.toString(), data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (moduleId || peekModule) router.push(`/${workspaceSlug}/projects/${data.project}/modules`);
|
if (moduleId || peekModule) router.push(`/${workspaceSlug}/projects/${data.project_id}/modules`);
|
||||||
handleClose();
|
handleClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -23,8 +23,8 @@ const defaultValues: Partial<IModule> = {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
status: "backlog",
|
status: "backlog",
|
||||||
lead: null,
|
lead_id: null,
|
||||||
members: [],
|
member_ids: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleForm: React.FC<Props> = ({
|
export const ModuleForm: React.FC<Props> = ({
|
||||||
@ -43,12 +43,12 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
reset,
|
reset,
|
||||||
} = useForm<IModule>({
|
} = useForm<IModule>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
project: projectId,
|
project_id: projectId,
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
description: data?.description || "",
|
description: data?.description || "",
|
||||||
status: data?.status || "backlog",
|
status: data?.status || "backlog",
|
||||||
lead: data?.lead || null,
|
lead_id: data?.lead_id || null,
|
||||||
members: data?.members || [],
|
member_ids: data?.member_ids || [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
{!status && (
|
{!status && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="project"
|
name="project_id"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectDropdown
|
<ProjectDropdown
|
||||||
@ -184,7 +184,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
<ModuleStatusSelect control={control} error={errors.status} tabIndex={5} />
|
<ModuleStatusSelect control={control} error={errors.status} tabIndex={5} />
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="lead"
|
name="lead_id"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectMemberDropdown
|
<ProjectMemberDropdown
|
||||||
@ -201,7 +201,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="members"
|
name="member_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ProjectMemberDropdown
|
<ProjectMemberDropdown
|
||||||
|
@ -29,7 +29,9 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center rounded"
|
className="relative flex h-full w-full items-center rounded"
|
||||||
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
|
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -65,7 +67,9 @@ export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center gap-2"
|
className="relative flex h-full w-full items-center gap-2"
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
|
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
|
||||||
<h6 className="flex-grow truncate text-sm font-medium">{moduleDetails?.name}</h6>
|
<h6 className="flex-grow truncate text-sm font-medium">{moduleDetails?.name}</h6>
|
||||||
|
@ -22,7 +22,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
|||||||
const payload: any = { ...data };
|
const payload: any = { ...data };
|
||||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||||
|
|
||||||
await updateModuleDetails(workspaceSlug.toString(), module.project, module.id, payload);
|
await updateModuleDetails(workspaceSlug.toString(), module.project_id, module.id, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const blockFormat = (blocks: string[]) =>
|
const blockFormat = (blocks: string[]) =>
|
||||||
|
@ -24,8 +24,8 @@ const defaultValues: Partial<IModule> = {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
status: "backlog",
|
status: "backlog",
|
||||||
lead: null,
|
lead_id: null,
|
||||||
members: [],
|
member_ids: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||||
@ -51,7 +51,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
const handleCreateModule = async (payload: Partial<IModule>) => {
|
const handleCreateModule = async (payload: Partial<IModule>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project_id ?? projectId.toString();
|
||||||
await createModule(workspaceSlug.toString(), selectedProjectId, payload)
|
await createModule(workspaceSlug.toString(), selectedProjectId, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
handleClose();
|
handleClose();
|
||||||
@ -81,7 +81,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
|
const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId || !data) return;
|
if (!workspaceSlug || !projectId || !data) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project_id ?? projectId.toString();
|
||||||
await updateModuleDetails(workspaceSlug.toString(), selectedProjectId, data.id, payload)
|
await updateModuleDetails(workspaceSlug.toString(), selectedProjectId, data.id, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
handleClose();
|
handleClose();
|
||||||
@ -129,8 +129,8 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
// if data is present, set active project to the project of the
|
// if data is present, set active project to the project of the
|
||||||
// issue. This has more priority than the project in the url.
|
// issue. This has more priority than the project in the url.
|
||||||
if (data && data.project) {
|
if (data && data.project_id) {
|
||||||
setActiveProject(data.project);
|
setActiveProject(data.project_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
|
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useModule, useUser } from "hooks/store";
|
import { useEventTracker, useMember, useModule, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
|
import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
|
||||||
@ -37,6 +37,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
const { setTrackElement, captureEvent } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
@ -147,8 +148,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||||
? "0 Issue"
|
? "0 Issue"
|
||||||
: moduleTotalIssues === moduleDetails.completed_issues
|
: moduleTotalIssues === moduleDetails.completed_issues
|
||||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
||||||
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
|
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
|
||||||
: "0 Issue";
|
: "0 Issue";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -163,7 +164,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
||||||
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project}/modules/${moduleDetails.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
|
||||||
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
|
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@ -195,13 +196,14 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||||
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
|
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
|
||||||
</div>
|
</div>
|
||||||
{moduleDetails.members_detail.length > 0 && (
|
{moduleDetails.member_ids?.length > 0 && (
|
||||||
<Tooltip tooltipContent={`${moduleDetails.members_detail.length} Members`}>
|
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`}>
|
||||||
<div className="flex cursor-default items-center gap-1">
|
<div className="flex cursor-default items-center gap-1">
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{moduleDetails.members_detail.map((member) => (
|
{moduleDetails.member_ids.map((member_id) => {
|
||||||
<Avatar key={member.id} name={member.display_name} src={member.avatar} />
|
const member = getUserDetails(member_id);
|
||||||
))}
|
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||||
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
|
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useModule, useUser, useEventTracker } from "hooks/store";
|
import { useModule, useUser, useEventTracker, useMember } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
|
import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
|
||||||
@ -37,6 +37,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
const { setTrackElement, captureEvent } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
@ -153,7 +154,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
||||||
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project}/modules/${moduleDetails.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
|
||||||
<div className="group flex w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 flex-col sm:flex-row px-5 py-6 text-sm hover:bg-custom-background-90">
|
<div className="group flex w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 flex-col sm:flex-row px-5 py-6 text-sm hover:bg-custom-background-90">
|
||||||
<div className="relative flex w-full items-center gap-3 justify-between overflow-hidden">
|
<div className="relative flex w-full items-center gap-3 justify-between overflow-hidden">
|
||||||
<div className="relative w-full flex items-center gap-3 overflow-hidden">
|
<div className="relative w-full flex items-center gap-3 overflow-hidden">
|
||||||
@ -206,13 +207,14 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 relative flex items-center gap-3">
|
<div className="flex-shrink-0 relative flex items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${moduleDetails.members_detail.length} Members`}>
|
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center gap-1">
|
<div className="flex w-10 cursor-default items-center justify-center gap-1">
|
||||||
{moduleDetails.members_detail.length > 0 ? (
|
{moduleDetails.member_ids.length > 0 ? (
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{moduleDetails.members_detail.map((member) => (
|
{moduleDetails.member_ids.map((member_id) => {
|
||||||
<Avatar key={member.id} name={member.display_name} src={member.avatar} />
|
const member = getUserDetails(member_id);
|
||||||
))}
|
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||||
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||||
|
@ -37,8 +37,8 @@ import { EUserProjectRoles } from "constants/project";
|
|||||||
import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker";
|
import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
const defaultValues: Partial<IModule> = {
|
const defaultValues: Partial<IModule> = {
|
||||||
lead: "",
|
lead_id: "",
|
||||||
members: [],
|
member_ids: [],
|
||||||
start_date: null,
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
status: "backlog",
|
status: "backlog",
|
||||||
@ -323,8 +323,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<CustomSelect
|
<CustomSelect
|
||||||
customButton={
|
customButton={
|
||||||
<span
|
<span
|
||||||
className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${
|
||||||
}`}
|
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
color: moduleStatus ? moduleStatus.color : "#a3a3a2",
|
color: moduleStatus ? moduleStatus.color : "#a3a3a2",
|
||||||
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
|
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
|
||||||
@ -373,13 +374,15 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
ref={startDateButtonRef}
|
ref={startDateButtonRef}
|
||||||
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
|
||||||
}`}
|
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||||
|
}`}
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${watch("start_date") ? "" : "text-custom-text-400"
|
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
|
||||||
}`}
|
watch("start_date") ? "" : "text-custom-text-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{renderFormattedDate(startDate) ?? "No date selected"}
|
{renderFormattedDate(startDate) ?? "No date selected"}
|
||||||
</span>
|
</span>
|
||||||
@ -427,13 +430,15 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
ref={endDateButtonRef}
|
ref={endDateButtonRef}
|
||||||
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
|
||||||
}`}
|
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||||
|
}`}
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${watch("target_date") ? "" : "text-custom-text-400"
|
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
|
||||||
}`}
|
watch("target_date") ? "" : "text-custom-text-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{renderFormattedDate(endDate) ?? "No date selected"}
|
{renderFormattedDate(endDate) ?? "No date selected"}
|
||||||
</span>
|
</span>
|
||||||
@ -485,13 +490,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="lead"
|
name="lead_id"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<ProjectMemberDropdown
|
<ProjectMemberDropdown
|
||||||
value={value ?? null}
|
value={value ?? null}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
submitChanges({ lead: val });
|
submitChanges({ lead_id: val });
|
||||||
}}
|
}}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
@ -509,13 +514,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="members"
|
name="member_ids"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<ProjectMemberDropdown
|
<ProjectMemberDropdown
|
||||||
value={value ?? []}
|
value={value ?? []}
|
||||||
onChange={(val: string[]) => {
|
onChange={(val: string[]) => {
|
||||||
submitChanges({ members: val });
|
submitChanges({ member_ids: val });
|
||||||
}}
|
}}
|
||||||
multiple
|
multiple
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
@ -59,6 +59,16 @@ export class IssueService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async retrieveIssues(workspaceSlug: string, projectId: string, issueIds: string[]): Promise<TIssue[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/list/`, {
|
||||||
|
params: { issues: issueIds.join(",") },
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueActivity[]> {
|
async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueActivity[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
@ -102,7 +102,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
get currentProjectCycleIds() {
|
get currentProjectCycleIds() {
|
||||||
const projectId = this.rootStore.app.router.projectId;
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project === projectId);
|
let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project_id === projectId);
|
||||||
allCycles = sortBy(allCycles, [(c) => c.sort_order]);
|
allCycles = sortBy(allCycles, [(c) => c.sort_order]);
|
||||||
const allCycleIds = allCycles.map((c) => c.id);
|
const allCycleIds = allCycles.map((c) => c.id);
|
||||||
return allCycleIds;
|
return allCycleIds;
|
||||||
@ -116,7 +116,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
||||||
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
||||||
return c.project === projectId && hasEndDatePassed;
|
return c.project_id === projectId && hasEndDatePassed;
|
||||||
});
|
});
|
||||||
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
|
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
|
||||||
const completedCycleIds = completedCycles.map((c) => c.id);
|
const completedCycleIds = completedCycles.map((c) => c.id);
|
||||||
@ -131,7 +131,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
||||||
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
|
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
|
||||||
return c.project === projectId && isStartDateUpcoming;
|
return c.project_id === projectId && isStartDateUpcoming;
|
||||||
});
|
});
|
||||||
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
|
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
|
||||||
const upcomingCycleIds = upcomingCycles.map((c) => c.id);
|
const upcomingCycleIds = upcomingCycles.map((c) => c.id);
|
||||||
@ -146,7 +146,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
||||||
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
||||||
return c.project === projectId && !hasEndDatePassed;
|
return c.project_id === projectId && !hasEndDatePassed;
|
||||||
});
|
});
|
||||||
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
|
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
|
||||||
const incompleteCycleIds = incompleteCycles.map((c) => c.id);
|
const incompleteCycleIds = incompleteCycles.map((c) => c.id);
|
||||||
@ -160,7 +160,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
const projectId = this.rootStore.app.router.projectId;
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
|
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
|
||||||
(c) => c.project === projectId && !c.start_date && !c.end_date
|
(c) => c.project_id === projectId && !c.start_date && !c.end_date
|
||||||
);
|
);
|
||||||
draftCycles = sortBy(draftCycles, [(c) => c.sort_order]);
|
draftCycles = sortBy(draftCycles, [(c) => c.sort_order]);
|
||||||
const draftCycleIds = draftCycles.map((c) => c.id);
|
const draftCycleIds = draftCycles.map((c) => c.id);
|
||||||
@ -174,7 +174,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
const projectId = this.rootStore.app.router.projectId;
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
if (!projectId) return null;
|
if (!projectId) return null;
|
||||||
const activeCycle = Object.keys(this.activeCycleIdMap ?? {}).find(
|
const activeCycle = Object.keys(this.activeCycleIdMap ?? {}).find(
|
||||||
(cycleId) => this.cycleMap?.[cycleId]?.project === projectId
|
(cycleId) => this.cycleMap?.[cycleId]?.project_id === projectId
|
||||||
);
|
);
|
||||||
return activeCycle || null;
|
return activeCycle || null;
|
||||||
}
|
}
|
||||||
@ -202,7 +202,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
getProjectCycleIds = computedFn((projectId: string): string[] | null => {
|
getProjectCycleIds = computedFn((projectId: string): string[] | null => {
|
||||||
if (!this.fetchedMap[projectId]) return null;
|
if (!this.fetchedMap[projectId]) return null;
|
||||||
|
|
||||||
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId);
|
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId);
|
||||||
cycles = sortBy(cycles, [(c) => c.sort_order]);
|
cycles = sortBy(cycles, [(c) => c.sort_order]);
|
||||||
const cycleIds = cycles.map((c) => c.id);
|
const cycleIds = cycles.map((c) => c.id);
|
||||||
return cycleIds || null;
|
return cycleIds || null;
|
||||||
|
@ -54,7 +54,13 @@ export interface ICycleIssues {
|
|||||||
data: TIssue,
|
data: TIssue,
|
||||||
cycleId?: string | undefined
|
cycleId?: string | undefined
|
||||||
) => Promise<TIssue>;
|
) => Promise<TIssue>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<TIssue>;
|
addIssueToCycle: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
issueIds: string[],
|
||||||
|
fetchAddedIssues?: boolean
|
||||||
|
) => Promise<void>;
|
||||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
||||||
transferIssuesFromCycle: (
|
transferIssuesFromCycle: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -182,7 +188,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
if (!cycleId) throw new Error("Cycle Id is required");
|
if (!cycleId) throw new Error("Cycle Id is required");
|
||||||
|
|
||||||
const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data);
|
const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data);
|
||||||
await this.addIssueToCycle(workspaceSlug, projectId, cycleId, [response.id]);
|
await this.addIssueToCycle(workspaceSlug, projectId, cycleId, [response.id], false);
|
||||||
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -265,21 +271,33 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
addIssueToCycle = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
issueIds: string[],
|
||||||
|
fetchAddedIssues = true
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
||||||
issues: issueIds,
|
issues: issueIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
|
update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
|
||||||
});
|
});
|
||||||
issueIds.forEach((issueId) => {
|
issueIds.forEach((issueId) => {
|
||||||
|
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
||||||
|
if (issueCycleId && issueCycleId !== cycleId) {
|
||||||
|
runInAction(() => {
|
||||||
|
pull(this.issues[issueCycleId], issueId);
|
||||||
|
});
|
||||||
|
}
|
||||||
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });
|
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });
|
||||||
});
|
});
|
||||||
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||||
|
|
||||||
return issueToCycle;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export interface IIssueStoreActions {
|
|||||||
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
|
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
|
||||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
|
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<TIssue>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
||||||
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
|
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
|
||||||
removeModulesFromIssue: (
|
removeModulesFromIssue: (
|
||||||
@ -123,15 +123,15 @@ export class IssueStore implements IIssueStore {
|
|||||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
const cycle = await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
|
await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
cycleId,
|
cycleId,
|
||||||
issueIds
|
issueIds,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
if (issueIds && issueIds.length > 0)
|
if (issueIds && issueIds.length > 0)
|
||||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
||||||
return cycle;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||||
|
@ -5,11 +5,14 @@ import { action, makeObservable, observable, runInAction } from "mobx";
|
|||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
//services
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
|
|
||||||
export type IIssueStore = {
|
export type IIssueStore = {
|
||||||
// observables
|
// observables
|
||||||
issuesMap: Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
|
issuesMap: Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
|
||||||
// actions
|
// actions
|
||||||
|
getIssues(workspaceSlug: string, projectId: string, issueIds: string[]): Promise<TIssue[]>;
|
||||||
addIssue(issues: TIssue[], shouldReplace?: boolean): void;
|
addIssue(issues: TIssue[], shouldReplace?: boolean): void;
|
||||||
updateIssue(issueId: string, issue: Partial<TIssue>): void;
|
updateIssue(issueId: string, issue: Partial<TIssue>): void;
|
||||||
removeIssue(issueId: string): void;
|
removeIssue(issueId: string): void;
|
||||||
@ -21,6 +24,8 @@ export type IIssueStore = {
|
|||||||
export class IssueStore implements IIssueStore {
|
export class IssueStore implements IIssueStore {
|
||||||
// observables
|
// observables
|
||||||
issuesMap: { [issue_id: string]: TIssue } = {};
|
issuesMap: { [issue_id: string]: TIssue } = {};
|
||||||
|
// service
|
||||||
|
issueService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
@ -31,6 +36,8 @@ export class IssueStore implements IIssueStore {
|
|||||||
updateIssue: action,
|
updateIssue: action,
|
||||||
removeIssue: action,
|
removeIssue: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.issueService = new IssueService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
@ -48,6 +55,18 @@ export class IssueStore implements IIssueStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => {
|
||||||
|
const issues = await this.issueService.retrieveIssues(workspaceSlug, projectId, issueIds);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
issues.forEach((issue) => {
|
||||||
|
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description This method will update the issue in the issuesMap
|
* @description This method will update the issue in the issuesMap
|
||||||
* @param {string} issueId
|
* @param {string} issueId
|
||||||
|
@ -52,7 +52,13 @@ export interface IModuleIssues {
|
|||||||
data: TIssue,
|
data: TIssue,
|
||||||
moduleId?: string | undefined
|
moduleId?: string | undefined
|
||||||
) => Promise<TIssue | undefined>;
|
) => Promise<TIssue | undefined>;
|
||||||
addIssuesToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
addIssuesToModule: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string,
|
||||||
|
issueIds: string[],
|
||||||
|
fetchAddedIssues?: boolean
|
||||||
|
) => Promise<void>;
|
||||||
removeIssuesFromModule: (
|
removeIssuesFromModule: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -187,7 +193,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
if (!moduleId) throw new Error("Module Id is required");
|
if (!moduleId) throw new Error("Module Id is required");
|
||||||
|
|
||||||
const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data);
|
const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data);
|
||||||
await this.addIssuesToModule(workspaceSlug, projectId, moduleId, [response.id]);
|
await this.addIssuesToModule(workspaceSlug, projectId, moduleId, [response.id], false);
|
||||||
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -269,12 +275,20 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addIssuesToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
addIssuesToModule = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string,
|
||||||
|
issueIds: string[],
|
||||||
|
fetchAddedIssues = true
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const issueToModule = await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, {
|
await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, {
|
||||||
issues: issueIds,
|
issues: issueIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
||||||
if (!moduleIssueIds) return [...issueIds];
|
if (!moduleIssueIds) return [...issueIds];
|
||||||
@ -289,8 +303,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||||
|
|
||||||
return issueToModule;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -356,7 +368,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
moduleIds.forEach((moduleId) => {
|
moduleIds.forEach((moduleId) => {
|
||||||
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
||||||
if (moduleIssueIds.includes(issueId)) return moduleIssueIds;
|
if (moduleIssueIds.includes(issueId)) return pull(moduleIssueIds, issueId);
|
||||||
else return uniq(concat(moduleIssueIds, [issueId]));
|
else return uniq(concat(moduleIssueIds, [issueId]));
|
||||||
});
|
});
|
||||||
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
|
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
|
||||||
|
@ -99,7 +99,7 @@ export class ModulesStore implements IModuleStore {
|
|||||||
get projectModuleIds() {
|
get projectModuleIds() {
|
||||||
const projectId = this.rootStore.app.router.projectId;
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
|
let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId);
|
||||||
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
||||||
const projectModuleIds = projectModules.map((m) => m.id);
|
const projectModuleIds = projectModules.map((m) => m.id);
|
||||||
return projectModuleIds || null;
|
return projectModuleIds || null;
|
||||||
@ -119,7 +119,7 @@ export class ModulesStore implements IModuleStore {
|
|||||||
getProjectModuleIds = computedFn((projectId: string) => {
|
getProjectModuleIds = computedFn((projectId: string) => {
|
||||||
if (!this.fetchedMap[projectId]) return null;
|
if (!this.fetchedMap[projectId]) return null;
|
||||||
|
|
||||||
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
|
let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId);
|
||||||
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
||||||
const projectModuleIds = projectModules.map((m) => m.id);
|
const projectModuleIds = projectModules.map((m) => m.id);
|
||||||
return projectModuleIds;
|
return projectModuleIds;
|
||||||
|
Loading…
Reference in New Issue
Block a user