forked from github/plane
chore: implemented multiple modules select in the issues (#3484)
* fix: add multiple module in an issue * feat: implemented multiple modules select in the issue detail and issue peekoverview and resolved build errors. * feat: handled module parameters type error in the issue create and draft modal * feat: handled UI for modules select dropdown * fix: delete module activity updated * ui: module issue activity * fix: module search endpoint and issue fetch in the modules * fix: module ids optimized * fix: replaced module_id from boolean to array of module Id's in module search modal params --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
c6d6b9a0e9
commit
804dd8300d
@ -562,7 +562,7 @@ class IssueSerializer(DynamicBaseSerializer):
|
|||||||
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
module_ids = serializers.SerializerMethodField()
|
||||||
|
|
||||||
# Many to many
|
# Many to many
|
||||||
label_ids = serializers.PrimaryKeyRelatedField(
|
label_ids = serializers.PrimaryKeyRelatedField(
|
||||||
@ -597,7 +597,7 @@ class IssueSerializer(DynamicBaseSerializer):
|
|||||||
"project_id",
|
"project_id",
|
||||||
"parent_id",
|
"parent_id",
|
||||||
"cycle_id",
|
"cycle_id",
|
||||||
"module_id",
|
"module_ids",
|
||||||
"label_ids",
|
"label_ids",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"sub_issues_count",
|
"sub_issues_count",
|
||||||
@ -613,6 +613,10 @@ class IssueSerializer(DynamicBaseSerializer):
|
|||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
def get_module_ids(self, obj):
|
||||||
|
# Access the prefetched modules and extract module IDs
|
||||||
|
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
|
||||||
|
|
||||||
|
|
||||||
class IssueLiteSerializer(DynamicBaseSerializer):
|
class IssueLiteSerializer(DynamicBaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
@ -35,17 +35,26 @@ urlpatterns = [
|
|||||||
name="project-modules",
|
name="project-modules",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
|
"post": "create_issue_modules",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-module",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
|
||||||
|
ModuleIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create_module_issues",
|
||||||
"get": "list",
|
"get": "list",
|
||||||
"post": "create",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-module-issues",
|
name="project-module-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
@ -599,16 +599,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
.filter(project_id=project_id)
|
.filter(project_id=project_id)
|
||||||
.filter(workspace__slug=slug)
|
.filter(workspace__slug=slug)
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.order_by(order_by)
|
.order_by(order_by)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
|
@ -100,7 +100,7 @@ def dashboard_assigned_issues(self, request, slug):
|
|||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_relation",
|
"issue_relation",
|
||||||
@ -110,7 +110,6 @@ def dashboard_assigned_issues(self, request, slug):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -221,9 +220,8 @@ def dashboard_created_issues(self, request, slug):
|
|||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
|
@ -95,7 +95,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
||||||
)
|
)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("labels", "assignees")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_inbox",
|
"issue_inbox",
|
||||||
@ -105,7 +105,6 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
|
@ -112,12 +112,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
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"))
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_reactions",
|
"issue_reactions",
|
||||||
@ -125,7 +121,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -1087,12 +1082,31 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
.filter(archived_at__isnull=False)
|
.filter(archived_at__isnull=False)
|
||||||
.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"))
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.select_related("parent")
|
.annotate(
|
||||||
.prefetch_related("assignees")
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.prefetch_related("labels")
|
.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")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
@ -1120,22 +1134,6 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
|
||||||
.annotate(module_id=F("issue_module__module_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")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
@ -1681,18 +1679,37 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
.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"))
|
||||||
.filter(is_draft=True)
|
.filter(is_draft=True)
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_reactions",
|
"issue_reactions",
|
||||||
queryset=IssueReaction.objects.select_related("actor"),
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.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")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
@ -1719,22 +1736,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
|
||||||
.annotate(module_id=F("issue_module__module_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")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
|
@ -7,6 +7,8 @@ from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
|||||||
from django.core import serializers
|
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
|
||||||
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -296,23 +298,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"issue", flat=True
|
"issue", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
issue_activity.delay(
|
_ = [
|
||||||
type="module.activity.deleted",
|
issue_activity.delay(
|
||||||
requested_data=json.dumps(
|
type="module.activity.deleted",
|
||||||
{
|
requested_data=json.dumps({"module_id": str(pk)}),
|
||||||
"module_id": str(pk),
|
actor_id=str(request.user.id),
|
||||||
"module_name": str(module.name),
|
issue_id=str(issue),
|
||||||
"issues": [str(issue_id) for issue_id in module_issues],
|
project_id=project_id,
|
||||||
}
|
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||||
),
|
epoch=int(timezone.now().timestamp()),
|
||||||
actor_id=str(request.user.id),
|
notification=True,
|
||||||
issue_id=str(pk),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
project_id=str(project_id),
|
)
|
||||||
current_instance=None,
|
for issue in module_issues
|
||||||
epoch=int(timezone.now().timestamp()),
|
]
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
module.delete()
|
module.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@ -332,62 +331,18 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
|
||||||
parent=OuterRef("issue")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.filter(module_id=self.kwargs.get("module_id"))
|
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("module")
|
|
||||||
.select_related("issue", "issue__state", "issue__project")
|
|
||||||
.prefetch_related("issue__assignees", "issue__labels")
|
|
||||||
.prefetch_related("module__members")
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
def get_queryset(self):
|
||||||
def list(self, request, slug, project_id, module_id):
|
return (
|
||||||
fields = [
|
Issue.objects.filter(
|
||||||
field
|
project_id=self.kwargs.get("project_id"),
|
||||||
for field in request.GET.get("fields", "").split(",")
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
if field
|
issue_module__module_id=self.kwargs.get("module_id")
|
||||||
]
|
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
issues = (
|
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_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)
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.filter(workspace__slug=slug)
|
.prefetch_related("labels", "assignees")
|
||||||
.select_related("project")
|
.prefetch_related('issue_module__module')
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.order_by(order_by)
|
|
||||||
.filter(**filters)
|
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -403,105 +358,118 @@ class ModuleIssueViewSet(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")
|
||||||
)
|
)
|
||||||
)
|
).distinct()
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id, module_id):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
serializer = IssueSerializer(
|
serializer = IssueSerializer(
|
||||||
issues, many=True, fields=fields if fields else None
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, module_id):
|
# create multiple issues inside a module
|
||||||
|
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 len(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,
|
||||||
)
|
)
|
||||||
module = Module.objects.get(
|
project = Project.objects.get(pk=project_id)
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
)
|
[
|
||||||
|
ModuleIssue(
|
||||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
issue_id=str(issue),
|
||||||
|
module_id=module_id,
|
||||||
update_module_issue_activity = []
|
project_id=project_id,
|
||||||
records_to_update = []
|
workspace_id=project.workspace_id,
|
||||||
record_to_create = []
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
for issue in issues:
|
|
||||||
module_issue = [
|
|
||||||
module_issue
|
|
||||||
for module_issue in module_issues
|
|
||||||
if str(module_issue.issue_id) in issues
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(module_issue):
|
|
||||||
if module_issue[0].module_id != module_id:
|
|
||||||
update_module_issue_activity.append(
|
|
||||||
{
|
|
||||||
"old_module_id": str(module_issue[0].module_id),
|
|
||||||
"new_module_id": str(module_id),
|
|
||||||
"issue_id": str(module_issue[0].issue_id),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
module_issue[0].module_id = module_id
|
|
||||||
records_to_update.append(module_issue[0])
|
|
||||||
else:
|
|
||||||
record_to_create.append(
|
|
||||||
ModuleIssue(
|
|
||||||
module=module,
|
|
||||||
issue_id=issue,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace=module.workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
for issue in issues
|
||||||
ModuleIssue.objects.bulk_create(
|
],
|
||||||
record_to_create,
|
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
# Bulk Update the activity
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.created",
|
||||||
|
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue),
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
issues = (self.get_queryset().filter(pk__in=issues))
|
||||||
|
serializer = IssueSerializer(issues , many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
ModuleIssue.objects.bulk_update(
|
# create multiple module inside an issue
|
||||||
records_to_update,
|
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||||
["module"],
|
modules = request.data.get("modules", [])
|
||||||
|
if not len(modules):
|
||||||
|
return Response(
|
||||||
|
{"error": "Modules are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ModuleIssue(
|
||||||
|
issue_id=issue_id,
|
||||||
|
module_id=module,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for module in modules
|
||||||
|
],
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
# Bulk Update the activity
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.created",
|
||||||
|
requested_data=json.dumps({"module_id": module}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=issue_id,
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for module in modules
|
||||||
|
]
|
||||||
|
|
||||||
# Capture Issue Activity
|
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||||
issue_activity.delay(
|
serializer = IssueSerializer(issue)
|
||||||
type="module.activity.created",
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
requested_data=json.dumps({"modules_list": issues}),
|
|
||||||
actor_id=str(self.request.user.id),
|
|
||||||
issue_id=None,
|
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
|
||||||
current_instance=json.dumps(
|
|
||||||
{
|
|
||||||
"updated_module_issues": update_module_issue_activity,
|
|
||||||
"created_module_issues": serializers.serialize(
|
|
||||||
"json", record_to_create
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
|
|
||||||
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, 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(
|
||||||
@ -512,16 +480,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||||
{
|
|
||||||
"module_id": str(module_id),
|
|
||||||
"issues": [str(issue_id)],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
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=None,
|
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"),
|
||||||
|
@ -228,7 +228,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
parent = request.query_params.get("parent", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
issue_relation = request.query_params.get("issue_relation", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
module = request.query_params.get("module", "false")
|
module = request.query_params.get("module", False)
|
||||||
sub_issue = request.query_params.get("sub_issue", "false")
|
sub_issue = request.query_params.get("sub_issue", "false")
|
||||||
|
|
||||||
issue_id = request.query_params.get("issue_id", False)
|
issue_id = request.query_params.get("issue_id", False)
|
||||||
@ -269,8 +269,8 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
if cycle == "true":
|
if cycle == "true":
|
||||||
issues = issues.exclude(issue_cycle__isnull=False)
|
issues = issues.exclude(issue_cycle__isnull=False)
|
||||||
|
|
||||||
if module == "true":
|
if module:
|
||||||
issues = issues.exclude(issue_module__isnull=False)
|
issues = issues.exclude(issue_module__module=module)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
issues.values(
|
issues.values(
|
||||||
|
@ -87,12 +87,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_reactions",
|
"issue_reactions",
|
||||||
@ -127,7 +123,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -150,13 +145,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
|
||||||
is_subscribed=Exists(
|
|
||||||
IssueSubscriber.objects.filter(
|
|
||||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
|
@ -1346,9 +1346,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
|
@ -30,6 +30,7 @@ from plane.app.serializers import IssueActivitySerializer
|
|||||||
from plane.bgtasks.notification_task import notifications
|
from plane.bgtasks.notification_task import notifications
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
|
|
||||||
|
|
||||||
# Track Changes in name
|
# Track Changes in name
|
||||||
def track_name(
|
def track_name(
|
||||||
requested_data,
|
requested_data,
|
||||||
@ -852,70 +853,26 @@ def create_module_issue_activity(
|
|||||||
requested_data = (
|
requested_data = (
|
||||||
json.loads(requested_data) if requested_data is not None else None
|
json.loads(requested_data) if requested_data is not None else None
|
||||||
)
|
)
|
||||||
current_instance = (
|
module = Module.objects.filter(pk=requested_data.get("module_id")).first()
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
issue = Issue.objects.filter(pk=issue_id).first()
|
||||||
)
|
if issue:
|
||||||
|
issue.updated_at = timezone.now()
|
||||||
# Updated Records:
|
issue.save(update_fields=["updated_at"])
|
||||||
updated_records = current_instance.get("updated_module_issues", [])
|
issue_activities.append(
|
||||||
created_records = json.loads(
|
IssueActivity(
|
||||||
current_instance.get("created_module_issues", [])
|
issue_id=issue_id,
|
||||||
)
|
actor_id=actor_id,
|
||||||
|
verb="created",
|
||||||
for updated_record in updated_records:
|
old_value="",
|
||||||
old_module = Module.objects.filter(
|
new_value=module.name,
|
||||||
pk=updated_record.get("old_module_id", None)
|
field="modules",
|
||||||
).first()
|
project_id=project_id,
|
||||||
new_module = Module.objects.filter(
|
workspace_id=workspace_id,
|
||||||
pk=updated_record.get("new_module_id", None)
|
comment=f"added module {module.name}",
|
||||||
).first()
|
new_identifier=requested_data.get("module_id"),
|
||||||
issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first()
|
epoch=epoch,
|
||||||
if issue:
|
|
||||||
issue.updated_at = timezone.now()
|
|
||||||
issue.save(update_fields=["updated_at"])
|
|
||||||
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=updated_record.get("issue_id"),
|
|
||||||
actor_id=actor_id,
|
|
||||||
verb="updated",
|
|
||||||
old_value=old_module.name,
|
|
||||||
new_value=new_module.name,
|
|
||||||
field="modules",
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
comment=f"updated module to ",
|
|
||||||
old_identifier=old_module.id,
|
|
||||||
new_identifier=new_module.id,
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for created_record in created_records:
|
|
||||||
module = Module.objects.filter(
|
|
||||||
pk=created_record.get("fields").get("module")
|
|
||||||
).first()
|
|
||||||
issue = Issue.objects.filter(
|
|
||||||
pk=created_record.get("fields").get("issue")
|
|
||||||
).first()
|
|
||||||
if issue:
|
|
||||||
issue.updated_at = timezone.now()
|
|
||||||
issue.save(update_fields=["updated_at"])
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=created_record.get("fields").get("issue"),
|
|
||||||
actor_id=actor_id,
|
|
||||||
verb="created",
|
|
||||||
old_value="",
|
|
||||||
new_value=module.name,
|
|
||||||
field="modules",
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
comment=f"added module {module.name}",
|
|
||||||
new_identifier=module.id,
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_module_issue_activity(
|
def delete_module_issue_activity(
|
||||||
@ -934,32 +891,26 @@ def delete_module_issue_activity(
|
|||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
)
|
)
|
||||||
|
module_name = current_instance.get("module_name")
|
||||||
module_id = requested_data.get("module_id", "")
|
current_issue = Issue.objects.filter(pk=issue_id).first()
|
||||||
module_name = requested_data.get("module_name", "")
|
if current_issue:
|
||||||
module = Module.objects.filter(pk=module_id).first()
|
current_issue.updated_at = timezone.now()
|
||||||
issues = requested_data.get("issues")
|
current_issue.save(update_fields=["updated_at"])
|
||||||
|
issue_activities.append(
|
||||||
for issue in issues:
|
IssueActivity(
|
||||||
current_issue = Issue.objects.filter(pk=issue).first()
|
issue_id=issue_id,
|
||||||
if issue:
|
actor_id=actor_id,
|
||||||
current_issue.updated_at = timezone.now()
|
verb="deleted",
|
||||||
current_issue.save(update_fields=["updated_at"])
|
old_value=module_name,
|
||||||
issue_activities.append(
|
new_value="",
|
||||||
IssueActivity(
|
field="modules",
|
||||||
issue_id=issue,
|
project_id=project_id,
|
||||||
actor_id=actor_id,
|
workspace_id=workspace_id,
|
||||||
verb="deleted",
|
comment=f"removed this issue from {module_name}",
|
||||||
old_value=module.name if module is not None else module_name,
|
old_identifier=requested_data.get("module_id") if requested_data.get("module_id") is not None else None,
|
||||||
new_value="",
|
epoch=epoch,
|
||||||
field="modules",
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
comment=f"removed this issue from {module.name if module is not None else module_name}",
|
|
||||||
old_identifier=module_id if module_id is not None else None,
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_link_activity(
|
def create_link_activity(
|
||||||
@ -1648,7 +1599,6 @@ def issue_activity(
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
|
|
||||||
|
|
||||||
if notification:
|
if notification:
|
||||||
notifications.delay(
|
notifications.delay(
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-24 18:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0057_auto_20240122_0901'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='issue',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='moduleissue',
|
||||||
|
unique_together={('issue', 'module')},
|
||||||
|
),
|
||||||
|
]
|
@ -134,11 +134,12 @@ class ModuleIssue(ProjectBaseModel):
|
|||||||
module = models.ForeignKey(
|
module = models.ForeignKey(
|
||||||
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
|
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
|
||||||
)
|
)
|
||||||
issue = models.OneToOneField(
|
issue = models.ForeignKey(
|
||||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_module"
|
"db.Issue", on_delete=models.CASCADE, related_name="issue_module"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
unique_together = ["issue", "module"]
|
||||||
verbose_name = "Module Issue"
|
verbose_name = "Module Issue"
|
||||||
verbose_name_plural = "Module Issues"
|
verbose_name_plural = "Module Issues"
|
||||||
db_table = "module_issues"
|
db_table = "module_issues"
|
||||||
|
2
packages/types/src/issues/issue.d.ts
vendored
2
packages/types/src/issues/issue.d.ts
vendored
@ -21,7 +21,7 @@ export type TIssue = {
|
|||||||
project_id: string;
|
project_id: string;
|
||||||
parent_id: string | null;
|
parent_id: string | null;
|
||||||
cycle_id: string | null;
|
cycle_id: string | null;
|
||||||
module_id: string | null;
|
module_ids: string[] | null;
|
||||||
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
2
packages/types/src/projects.d.ts
vendored
2
packages/types/src/projects.d.ts
vendored
@ -117,7 +117,7 @@ export type TProjectIssuesSearchParams = {
|
|||||||
parent?: boolean;
|
parent?: boolean;
|
||||||
issue_relation?: boolean;
|
issue_relation?: boolean;
|
||||||
cycle?: boolean;
|
cycle?: boolean;
|
||||||
module?: boolean;
|
module?: string[];
|
||||||
sub_issue?: boolean;
|
sub_issue?: boolean;
|
||||||
issue_id?: string;
|
issue_id?: string;
|
||||||
workspace_search: boolean;
|
workspace_search: boolean;
|
||||||
|
@ -216,7 +216,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={isCreateIssueModalOpen}
|
isOpen={isCreateIssueModalOpen}
|
||||||
onClose={() => toggleCreateIssueModal(false)}
|
onClose={() => toggleCreateIssueModal(false)}
|
||||||
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined}
|
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
|
||||||
storeType={createIssueStoreType}
|
storeType={createIssueStoreType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ export * from "./cycle";
|
|||||||
export * from "./date";
|
export * from "./date";
|
||||||
export * from "./estimate";
|
export * from "./estimate";
|
||||||
export * from "./module";
|
export * from "./module";
|
||||||
|
export * from "./module-select";
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
|
114
web/components/dropdowns/module-select/button.tsx
Normal file
114
web/components/dropdowns/module-select/button.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ChevronDown, X } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useModule } from "hooks/store";
|
||||||
|
// ui and components
|
||||||
|
import { DiceIcon, Tooltip } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TModuleSelectButton } from "./types";
|
||||||
|
|
||||||
|
export const ModuleSelectButton: FC<TModuleSelectButton> = observer((props) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
buttonClassName,
|
||||||
|
buttonVariant,
|
||||||
|
hideIcon,
|
||||||
|
hideText,
|
||||||
|
dropdownArrow,
|
||||||
|
dropdownArrowClassName,
|
||||||
|
showTooltip,
|
||||||
|
showCount,
|
||||||
|
} = props;
|
||||||
|
// hooks
|
||||||
|
const { getModuleById } = useModule();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
`w-full h-full relative overflow-hidden flex justify-between items-center gap-1 rounded text-sm px-2`,
|
||||||
|
buttonVariant === "border-with-text"
|
||||||
|
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
|
||||||
|
: ``,
|
||||||
|
buttonVariant === "border-without-text"
|
||||||
|
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
|
||||||
|
: ``,
|
||||||
|
buttonVariant === "background-with-text" ? `bg-custom-background-80` : ``,
|
||||||
|
buttonVariant === "background-without-text" ? `bg-custom-background-80` : ``,
|
||||||
|
buttonVariant === "transparent-with-text" ? `hover:bg-custom-background-80` : ``,
|
||||||
|
buttonVariant === "transparent-without-text" ? `hover:bg-custom-background-80` : ``,
|
||||||
|
buttonClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden h-full flex flex-wrap items-center gap-1">
|
||||||
|
{value && typeof value === "string" ? (
|
||||||
|
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||||
|
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
|
{!hideText && (
|
||||||
|
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
|
||||||
|
{getModuleById(value)?.name || placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : value && Array.isArray(value) && value.length > 0 ? (
|
||||||
|
showCount ? (
|
||||||
|
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||||
|
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
|
{!hideText && (
|
||||||
|
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
|
||||||
|
{value.length} Modules
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
value.map((moduleId) => {
|
||||||
|
const _module = getModuleById(moduleId);
|
||||||
|
if (!_module) return <></>;
|
||||||
|
return (
|
||||||
|
<div className="relative flex justify-between items-center gap-1 min-w-[60px] max-w-[84px] overflow-hidden bg-custom-background-80 px-1.5 py-1 rounded">
|
||||||
|
<Tooltip tooltipContent={_module?.name} disabled={!showTooltip}>
|
||||||
|
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||||
|
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
|
{!hideText && (
|
||||||
|
<span className="w-full truncate inline-block line-clamp-1 capitalize">{_module?.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip tooltipContent="Remove" disabled={!showTooltip}>
|
||||||
|
<span
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange(_module.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
!hideText && (
|
||||||
|
<div className="relative overflow-hidden flex items-center gap-1.5">
|
||||||
|
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
|
{!hideText && (
|
||||||
|
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dropdownArrow && (
|
||||||
|
<ChevronDown className={twMerge("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
2
web/components/dropdowns/module-select/index.ts
Normal file
2
web/components/dropdowns/module-select/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./button";
|
||||||
|
export * from "./select";
|
227
web/components/dropdowns/module-select/select.tsx
Normal file
227
web/components/dropdowns/module-select/select.tsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { FC, useEffect, useRef, useState, Fragment } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
|
import { Check, Search } from "lucide-react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
// hooks
|
||||||
|
import { useModule } from "hooks/store";
|
||||||
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
|
import { ModuleSelectButton } from "./";
|
||||||
|
// types
|
||||||
|
import { TModuleSelectDropdown, TModuleSelectDropdownOption } from "./types";
|
||||||
|
import { DiceIcon } from "@plane/ui";
|
||||||
|
|
||||||
|
export const ModuleSelectDropdown: FC<TModuleSelectDropdown> = observer((props) => {
|
||||||
|
// props
|
||||||
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
value = undefined,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Module",
|
||||||
|
multiple = false,
|
||||||
|
disabled = false,
|
||||||
|
className = "",
|
||||||
|
buttonContainerClassName = "",
|
||||||
|
buttonClassName = "",
|
||||||
|
buttonVariant = "transparent-with-text",
|
||||||
|
hideIcon = false,
|
||||||
|
dropdownArrow = false,
|
||||||
|
dropdownArrowClassName = "",
|
||||||
|
showTooltip = false,
|
||||||
|
showCount = false,
|
||||||
|
placement,
|
||||||
|
tabIndex,
|
||||||
|
button,
|
||||||
|
} = props;
|
||||||
|
// states
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
// refs
|
||||||
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
// popper-js refs
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
// popper-js init
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: placement ?? "bottom-start",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
options: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// store hooks
|
||||||
|
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
|
||||||
|
|
||||||
|
const moduleIds = getProjectModuleIds(projectId);
|
||||||
|
|
||||||
|
const options: TModuleSelectDropdownOption[] | undefined = moduleIds?.map((moduleId) => {
|
||||||
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
return {
|
||||||
|
value: moduleId,
|
||||||
|
query: `${moduleDetails?.name}`,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">{moduleDetails?.name}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
!multiple &&
|
||||||
|
options?.unshift({
|
||||||
|
value: undefined,
|
||||||
|
query: "No module",
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">No module</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredOptions =
|
||||||
|
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
|
// fetch modules of the project if not already present in the store
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
if (!moduleIds) fetchModules(workspaceSlug, projectId);
|
||||||
|
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
|
||||||
|
|
||||||
|
const openDropdown = () => {
|
||||||
|
if (isOpen) closeDropdown();
|
||||||
|
else {
|
||||||
|
setIsOpen(true);
|
||||||
|
if (referenceElement) referenceElement.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const closeDropdown = () => setIsOpen(false);
|
||||||
|
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||||
|
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||||
|
|
||||||
|
const comboboxProps: any = {};
|
||||||
|
if (multiple) comboboxProps.multiple = true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
as="div"
|
||||||
|
ref={dropdownRef}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
className={twMerge("h-full", className)}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
{...comboboxProps}
|
||||||
|
>
|
||||||
|
<Combobox.Button as={Fragment}>
|
||||||
|
{button ? (
|
||||||
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
className={twMerge(
|
||||||
|
"block h-full max-w-full outline-none",
|
||||||
|
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
|
||||||
|
buttonContainerClassName
|
||||||
|
)}
|
||||||
|
onClick={openDropdown}
|
||||||
|
>
|
||||||
|
{button}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
className={twMerge(
|
||||||
|
"block h-full max-w-full outline-none ",
|
||||||
|
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
|
||||||
|
buttonContainerClassName
|
||||||
|
)}
|
||||||
|
onClick={openDropdown}
|
||||||
|
>
|
||||||
|
<ModuleSelectButton
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
buttonClassName={buttonClassName}
|
||||||
|
buttonVariant={buttonVariant}
|
||||||
|
hideIcon={hideIcon}
|
||||||
|
hideText={["border-without-text", "background-without-text", "transparent-without-text"].includes(
|
||||||
|
buttonVariant
|
||||||
|
)}
|
||||||
|
dropdownArrow={dropdownArrow}
|
||||||
|
dropdownArrowClassName={dropdownArrowClassName}
|
||||||
|
showTooltip={showTooltip}
|
||||||
|
showCount={showCount}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Combobox.Button>
|
||||||
|
{isOpen && (
|
||||||
|
<Combobox.Options className="fixed z-10" static>
|
||||||
|
<div
|
||||||
|
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||||
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
|
<Combobox.Input
|
||||||
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
displayValue={(moduleIds: any) => {
|
||||||
|
const displayValueOptions: TModuleSelectDropdownOption[] | undefined = options?.filter((_module) =>
|
||||||
|
moduleIds.includes(_module.value)
|
||||||
|
);
|
||||||
|
return displayValueOptions?.map((_option) => _option.query).join(", ") || "Select Module";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
{filteredOptions ? (
|
||||||
|
filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
|
||||||
|
active ? "bg-custom-background-80" : ""
|
||||||
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
|
}
|
||||||
|
onClick={() => !multiple && closeDropdown()}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
<span className="flex-grow truncate">{option.content}</span>
|
||||||
|
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Combobox.Options>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
});
|
50
web/components/dropdowns/module-select/types.d.ts
vendored
Normal file
50
web/components/dropdowns/module-select/types.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { Placement } from "@popperjs/core";
|
||||||
|
import { TDropdownProps, TButtonVariants } from "../types";
|
||||||
|
|
||||||
|
type TModuleSelectDropdownRoot = Omit<
|
||||||
|
TDropdownProps,
|
||||||
|
"buttonClassName",
|
||||||
|
"buttonContainerClassName",
|
||||||
|
"buttonContainerClassName",
|
||||||
|
"className",
|
||||||
|
"disabled",
|
||||||
|
"hideIcon",
|
||||||
|
"placeholder",
|
||||||
|
"placement",
|
||||||
|
"tabIndex",
|
||||||
|
"tooltip"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TModuleSelectDropdownBase = {
|
||||||
|
value: string | string[] | undefined;
|
||||||
|
onChange: (moduleIds: undefined | string | (string | undefined)[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
buttonClassName?: string;
|
||||||
|
buttonVariant?: TButtonVariants;
|
||||||
|
hideIcon?: boolean;
|
||||||
|
dropdownArrow?: boolean;
|
||||||
|
dropdownArrowClassName?: string;
|
||||||
|
showTooltip?: boolean;
|
||||||
|
showCount?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TModuleSelectButton = TModuleSelectDropdownBase & { hideText?: boolean };
|
||||||
|
|
||||||
|
export type TModuleSelectDropdown = TModuleSelectDropdownBase & {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
className?: string;
|
||||||
|
buttonContainerClassName?: string;
|
||||||
|
placement?: Placement;
|
||||||
|
tabIndex?: number;
|
||||||
|
button?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TModuleSelectDropdownOption = {
|
||||||
|
value: string | undefined;
|
||||||
|
query: string;
|
||||||
|
content: JSX.Element;
|
||||||
|
};
|
@ -21,7 +21,7 @@ import {
|
|||||||
CycleDropdown,
|
CycleDropdown,
|
||||||
DateDropdown,
|
DateDropdown,
|
||||||
EstimateDropdown,
|
EstimateDropdown,
|
||||||
ModuleDropdown,
|
ModuleSelectDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectDropdown,
|
ProjectDropdown,
|
||||||
ProjectMemberDropdown,
|
ProjectMemberDropdown,
|
||||||
@ -152,7 +152,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
project_id: watch("project_id"),
|
project_id: watch("project_id"),
|
||||||
parent_id: watch("parent_id"),
|
parent_id: watch("parent_id"),
|
||||||
cycle_id: watch("cycle_id"),
|
cycle_id: watch("cycle_id"),
|
||||||
module_id: watch("module_id"),
|
module_ids: watch("module_ids"),
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -570,15 +570,17 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{projectDetails?.module_view && (
|
|
||||||
|
{projectDetails?.module_view && workspaceSlug && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="module_id"
|
name="module_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ModuleDropdown
|
<ModuleSelectDropdown
|
||||||
|
workspaceSlug={workspaceSlug?.toString()}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={value}
|
value={value || undefined}
|
||||||
onChange={(moduleId) => onChange(moduleId)}
|
onChange={(moduleId) => onChange(moduleId)}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
/>
|
/>
|
||||||
@ -586,6 +588,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
||||||
areEstimatesEnabledForProject(projectId) && (
|
areEstimatesEnabledForProject(projectId) && (
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -94,7 +94,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
cycle: cycleId.toString(),
|
cycle: cycleId.toString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (moduleId && !prePopulateDataProps?.module_id) {
|
if (moduleId && !prePopulateDataProps?.module_ids) {
|
||||||
setPreloadedData((prevData) => ({
|
setPreloadedData((prevData) => ({
|
||||||
...(prevData ?? {}),
|
...(prevData ?? {}),
|
||||||
...prePopulateDataProps,
|
...prePopulateDataProps,
|
||||||
@ -123,7 +123,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
cycle: cycleId.toString(),
|
cycle: cycleId.toString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (moduleId && !prePopulateDataProps?.module_id) {
|
if (moduleId && !prePopulateDataProps?.module_ids) {
|
||||||
setPreloadedData((prevData) => ({
|
setPreloadedData((prevData) => ({
|
||||||
...(prevData ?? {}),
|
...(prevData ?? {}),
|
||||||
...prePopulateDataProps,
|
...prePopulateDataProps,
|
||||||
@ -233,11 +233,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addIssueToModule = async (issueId: string, moduleId: string) => {
|
const addIssueToModule = async (issueId: string, moduleIds: string[]) => {
|
||||||
if (!workspaceSlug || !activeProject) return;
|
if (!workspaceSlug || !activeProject) return;
|
||||||
|
|
||||||
await moduleService.addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, {
|
await moduleService.addModulesToIssue(workspaceSlug as string, activeProject ?? "", issueId as string, {
|
||||||
issues: [issueId],
|
modules: moduleIds,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res.id, payload.cycle_id);
|
if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res.id, payload.cycle_id);
|
||||||
if (payload.module_id && payload.module_id !== "") await addIssueToModule(res.id, payload.module_id);
|
if (payload.module_ids && payload.module_ids.length > 0) await addIssueToModule(res.id, payload.module_ids);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -59,7 +59,7 @@ export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) =>
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||||
>
|
>
|
||||||
<span className="truncate"> {activity.new_value}</span>
|
<span className="truncate"> {activity.old_value}</span>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import xor from "lodash/xor";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ModuleDropdown } from "components/dropdowns";
|
import { ModuleSelectDropdown } from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -32,58 +33,56 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
|||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
const disableSelect = disabled || isUpdating;
|
const disableSelect = disabled || isUpdating;
|
||||||
|
|
||||||
const handleIssueModuleChange = async (moduleId: string | null) => {
|
const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => {
|
||||||
if (!issue || issue.module_id === moduleId) return;
|
if (!issue) return;
|
||||||
|
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
if (moduleId) await issueOperations.addIssueToModule?.(workspaceSlug, projectId, moduleId, [issueId]);
|
if (moduleIds === undefined && issue?.module_ids && issue?.module_ids.length > 0)
|
||||||
else await issueOperations.removeIssueFromModule?.(workspaceSlug, projectId, issue.module_id ?? "", issueId);
|
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue?.module_ids);
|
||||||
|
|
||||||
|
if (typeof moduleIds === "string" && moduleIds)
|
||||||
|
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]);
|
||||||
|
|
||||||
|
if (Array.isArray(moduleIds)) {
|
||||||
|
if (moduleIds.includes(undefined)) {
|
||||||
|
await issueOperations.removeModulesFromIssue?.(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
moduleIds.filter((x) => x != undefined) as string[]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const _moduleIds = xor(issue?.module_ids, moduleIds)[0];
|
||||||
|
if (_moduleIds) {
|
||||||
|
if (issue?.module_ids?.includes(_moduleIds))
|
||||||
|
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
|
||||||
|
else await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center gap-1 h-full", className)}>
|
<div className={cn(`flex items-center gap-1 h-full`, className)}>
|
||||||
<ModuleDropdown
|
<ModuleSelectDropdown
|
||||||
value={issue?.module_id ?? null}
|
workspaceSlug={workspaceSlug}
|
||||||
onChange={handleIssueModuleChange}
|
|
||||||
buttonVariant="transparent-with-text"
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
disabled={disableSelect}
|
value={issue?.module_ids?.length ? issue?.module_ids : undefined}
|
||||||
className="w-full group"
|
onChange={handleIssueModuleChange}
|
||||||
buttonContainerClassName="w-full text-left"
|
multiple={true}
|
||||||
buttonClassName={`text-sm ${issue?.module_id ? "" : "text-custom-text-400"}`}
|
|
||||||
placeholder="No module"
|
placeholder="No module"
|
||||||
hideIcon
|
|
||||||
dropdownArrow
|
|
||||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
|
||||||
/>
|
|
||||||
{/* <CustomSearchSelect
|
|
||||||
value={issue?.module_id}
|
|
||||||
onChange={(value: any) => handleIssueModuleChange(value)}
|
|
||||||
options={options}
|
|
||||||
customButton={
|
|
||||||
<div>
|
|
||||||
<Tooltip position="left" tooltipContent={`${issueModule?.name ?? "No module"}`}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
|
|
||||||
disableSelect ? "cursor-not-allowed" : ""
|
|
||||||
} max-w-[10rem]`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`flex items-center gap-1.5 truncate ${
|
|
||||||
issueModule ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
|
|
||||||
<span className="truncate">{issueModule?.name ?? "No module"}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
noChevron
|
|
||||||
disabled={disableSelect}
|
disabled={disableSelect}
|
||||||
/> */}
|
className={`w-full h-full group`}
|
||||||
|
buttonContainerClassName="w-full"
|
||||||
|
buttonClassName={`min-h-8 ${issue?.module_ids?.length ? `` : `text-custom-text-400`}`}
|
||||||
|
buttonVariant="transparent-with-text"
|
||||||
|
hideIcon={false}
|
||||||
|
dropdownArrow={true}
|
||||||
|
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||||
|
showTooltip={true}
|
||||||
|
/>
|
||||||
|
|
||||||
{isUpdating && <Spinner className="h-4 w-4" />}
|
{isUpdating && <Spinner className="h-4 w-4" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -29,13 +29,19 @@ export type TIssueOperations = {
|
|||||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToModule?: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||||
removeIssueFromModule?: (
|
removeIssueFromModule?: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
issueId: string
|
issueId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
removeModulesFromIssue?: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleIds: string[]
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueDetailRoot = {
|
export type TIssueDetailRoot = {
|
||||||
@ -57,8 +63,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
removeIssueFromCycle,
|
removeIssueFromCycle,
|
||||||
addIssueToModule,
|
addModulesToIssue,
|
||||||
removeIssueFromModule,
|
removeIssueFromModule,
|
||||||
|
removeModulesFromIssue,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const {
|
const {
|
||||||
issues: { removeIssue: removeArchivedIssue },
|
issues: { removeIssue: removeArchivedIssue },
|
||||||
@ -150,9 +157,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Module added to issue successfully",
|
title: "Module added to issue successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -182,6 +189,27 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeModulesFromIssue: async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleIds: string[]
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module removed from issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Module removed from issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module remove from issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Module remove from issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
is_archived,
|
is_archived,
|
||||||
@ -191,8 +219,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
removeArchivedIssue,
|
removeArchivedIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
removeIssueFromCycle,
|
removeIssueFromCycle,
|
||||||
addIssueToModule,
|
addModulesToIssue,
|
||||||
removeIssueFromModule,
|
removeIssueFromModule,
|
||||||
|
removeModulesFromIssue,
|
||||||
setToastAlert,
|
setToastAlert,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -286,7 +286,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{projectDetails?.module_view && (
|
{projectDetails?.module_view && (
|
||||||
<div className="flex items-center gap-2 h-8">
|
<div className="flex items-center gap-2 min-h-8 h-full">
|
||||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<span>Module</span>
|
<span>Module</span>
|
||||||
|
@ -46,11 +46,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const issueIds = data.map((i) => i.id);
|
const issueIds = data.map((i) => i.id);
|
||||||
await issues
|
await issues
|
||||||
.addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
|
.addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
|
||||||
.then((res) => {
|
|
||||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
|
||||||
fetchIssue(workspaceSlug, projectId, res.id);
|
|
||||||
})
|
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -69,7 +65,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
isOpen={moduleIssuesListModal}
|
isOpen={moduleIssuesListModal}
|
||||||
handleClose={() => setModuleIssuesListModal(false)}
|
handleClose={() => setModuleIssuesListModal(false)}
|
||||||
searchParams={{ module: true }}
|
searchParams={{ module: moduleId != undefined ? [moduleId.toString()] : [] }}
|
||||||
handleOnSubmit={handleAddIssuesToModule}
|
handleOnSubmit={handleAddIssuesToModule}
|
||||||
/>
|
/>
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
|
@ -44,7 +44,7 @@ export interface IBaseKanBanLayout {
|
|||||||
showLoader?: boolean;
|
showLoader?: boolean;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
storeType?: TCreateModalStoreTypes;
|
storeType?: TCreateModalStoreTypes;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const renderExistingIssueModal = moduleId || cycleId;
|
const renderExistingIssueModal = moduleId || cycleId;
|
||||||
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
|
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||||
|
|
||||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
@ -53,7 +53,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
storeType={EIssuesStoreType.MODULE}
|
storeType={EIssuesStoreType.MODULE}
|
||||||
addIssuesToView={(issueIds: string[]) => {
|
addIssuesToView={(issueIds: string[]) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
|
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
|
||||||
return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
|
return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +49,7 @@ interface IBaseListRoot {
|
|||||||
};
|
};
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
storeType: TCreateModalStoreTypes;
|
storeType: TCreateModalStoreTypes;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export const HeaderGroupByCard = observer(
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const renderExistingIssueModal = moduleId || cycleId;
|
const renderExistingIssueModal = moduleId || cycleId;
|
||||||
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
|
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||||
|
|
||||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
@ -51,7 +51,7 @@ export const ModuleListLayout: React.FC = observer(() => {
|
|||||||
storeType={EIssuesStoreType.MODULE}
|
storeType={EIssuesStoreType.MODULE}
|
||||||
addIssuesToView={(issueIds: string[]) => {
|
addIssuesToView={(issueIds: string[]) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
|
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
|
||||||
return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
|
return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
CycleDropdown,
|
CycleDropdown,
|
||||||
DateDropdown,
|
DateDropdown,
|
||||||
EstimateDropdown,
|
EstimateDropdown,
|
||||||
ModuleDropdown,
|
ModuleSelectDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectDropdown,
|
ProjectDropdown,
|
||||||
ProjectMemberDropdown,
|
ProjectMemberDropdown,
|
||||||
@ -44,7 +44,7 @@ const defaultValues: Partial<TIssue> = {
|
|||||||
assignee_ids: [],
|
assignee_ids: [],
|
||||||
label_ids: [],
|
label_ids: [],
|
||||||
cycle_id: null,
|
cycle_id: null,
|
||||||
module_id: null,
|
module_ids: null,
|
||||||
start_date: null,
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
};
|
};
|
||||||
@ -541,21 +541,24 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{projectDetails?.module_view && (
|
{projectDetails?.module_view && workspaceSlug && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="module_id"
|
name="module_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ModuleDropdown
|
<ModuleSelectDropdown
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={value}
|
value={value || undefined}
|
||||||
onChange={(moduleId) => {
|
onChange={(moduleId) => {
|
||||||
onChange(moduleId);
|
onChange(moduleId);
|
||||||
handleFormChange();
|
handleFormChange();
|
||||||
}}
|
}}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
tabIndex={13}
|
tabIndex={13}
|
||||||
|
multiple={true}
|
||||||
|
showCount={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -108,11 +108,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
fetchCycleDetails(workspaceSlug, activeProjectId, cycleId);
|
fetchCycleDetails(workspaceSlug, activeProjectId, cycleId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addIssueToModule = async (issue: TIssue, moduleId: string) => {
|
const addIssueToModule = async (issue: TIssue, moduleIds: string[]) => {
|
||||||
if (!workspaceSlug || !activeProjectId) return;
|
if (!workspaceSlug || !activeProjectId) return;
|
||||||
|
|
||||||
await moduleIssues.addIssueToModule(workspaceSlug, activeProjectId, moduleId, [issue.id]);
|
await moduleIssues.addModulesToIssue(workspaceSlug, activeProjectId, issue.id, moduleIds);
|
||||||
fetchModuleDetails(workspaceSlug, activeProjectId, moduleId);
|
moduleIds.forEach((moduleId) => fetchModuleDetails(workspaceSlug, activeProjectId, moduleId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateMoreToggleChange = (value: boolean) => {
|
const handleCreateMoreToggleChange = (value: boolean) => {
|
||||||
@ -139,8 +139,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
|
|
||||||
if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE)
|
if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE)
|
||||||
await addIssueToCycle(response, payload.cycle_id);
|
await addIssueToCycle(response, payload.cycle_id);
|
||||||
if (payload.module_id && payload.module_id !== "" && storeType !== EIssuesStoreType.MODULE)
|
if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE)
|
||||||
await addIssueToModule(response, payload.module_id);
|
await addIssueToModule(response, payload.module_ids);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -278,7 +278,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||||
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
|
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||||
}}
|
}}
|
||||||
onChange={handleFormChange}
|
onChange={handleFormChange}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
@ -292,7 +292,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||||
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
|
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||||
}}
|
}}
|
||||||
onClose={() => handleClose(false)}
|
onClose={() => handleClose(false)}
|
||||||
isCreateMoreToggleEnabled={createMore}
|
isCreateMoreToggleEnabled={createMore}
|
||||||
|
@ -203,7 +203,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{projectDetails?.module_view && (
|
{projectDetails?.module_view && (
|
||||||
<div className="flex w-full items-center gap-3 h-8">
|
<div className="flex w-full items-center gap-3 min-h-8 h-full">
|
||||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<span>Module</span>
|
<span>Module</span>
|
||||||
|
@ -28,8 +28,19 @@ export type TIssuePeekOperations = {
|
|||||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||||
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
removeIssueFromModule?: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string,
|
||||||
|
issueId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
removeModulesFromIssue?: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleIds: string[]
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
@ -48,7 +59,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
issue: { getIssueById, fetchIssue },
|
issue: { getIssueById, fetchIssue },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule } = useIssueDetail();
|
const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } =
|
||||||
|
useIssueDetail();
|
||||||
// state
|
// state
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
@ -143,9 +155,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Module added to issue successfully",
|
title: "Module added to issue successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -175,6 +187,27 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeModulesFromIssue: async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleIds: string[]
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module removed from issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Module removed from issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module remove from issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Module remove from issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
is_archived,
|
is_archived,
|
||||||
@ -184,8 +217,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
removeArchivedIssue,
|
removeArchivedIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
removeIssueFromCycle,
|
removeIssueFromCycle,
|
||||||
addIssueToModule,
|
addModulesToIssue,
|
||||||
removeIssueFromModule,
|
removeIssueFromModule,
|
||||||
|
removeModulesFromIssue,
|
||||||
setToastAlert,
|
setToastAlert,
|
||||||
onIssueUpdate,
|
onIssueUpdate,
|
||||||
]
|
]
|
||||||
|
@ -47,7 +47,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
<InstanceLayout>
|
<InstanceLayout>
|
||||||
<StoreWrapper>
|
<StoreWrapper>
|
||||||
<CrispWrapper user={currentUser}>
|
<CrispWrapper user={currentUser}>
|
||||||
<PosthogWrapper
|
{/* <PosthogWrapper
|
||||||
user={currentUser}
|
user={currentUser}
|
||||||
workspaceRole={currentWorkspaceRole}
|
workspaceRole={currentWorkspaceRole}
|
||||||
projectRole={currentProjectRole}
|
projectRole={currentProjectRole}
|
||||||
@ -55,7 +55,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
posthogHost={envConfig?.posthog_host || null}
|
posthogHost={envConfig?.posthog_host || null}
|
||||||
>
|
>
|
||||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||||
</PosthogWrapper>
|
</PosthogWrapper> */}
|
||||||
|
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||||
</CrispWrapper>
|
</CrispWrapper>
|
||||||
</StoreWrapper>
|
</StoreWrapper>
|
||||||
</InstanceLayout>
|
</InstanceLayout>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// services
|
// services
|
||||||
import { APIService } from "services/api.service";
|
import { APIService } from "services/api.service";
|
||||||
// types
|
// types
|
||||||
import type { IModule, TIssue, ILinkDetails, ModuleLink, TIssueMap } from "@plane/types";
|
import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
export class ModuleService extends APIService {
|
export class ModuleService extends APIService {
|
||||||
@ -63,22 +63,7 @@ export class ModuleService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string, queries?: any): Promise<TIssue[]> {
|
async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string, queries?: any): Promise<TIssue[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, {
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/`, {
|
||||||
params: queries,
|
|
||||||
})
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModuleIssuesWithParams(
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
moduleId: string,
|
|
||||||
queries?: any
|
|
||||||
): Promise<TIssue[] | { [key: string]: TIssue[] }> {
|
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, {
|
|
||||||
params: queries,
|
params: queries,
|
||||||
})
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
@ -92,15 +77,21 @@ export class ModuleService extends APIService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
data: { issues: string[] }
|
data: { issues: string[] }
|
||||||
): Promise<
|
): Promise<void> {
|
||||||
{
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/`, data)
|
||||||
issue: string;
|
.then((response) => response?.data)
|
||||||
issue_detail: TIssue;
|
.catch((error) => {
|
||||||
module: string;
|
throw error?.response?.data;
|
||||||
module_detail: IModule;
|
});
|
||||||
}[]
|
}
|
||||||
> {
|
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data)
|
async addModulesToIssue(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
data: { modules: string[] }
|
||||||
|
): Promise<void> {
|
||||||
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/modules/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
@ -111,17 +102,53 @@ export class ModuleService extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
bridgeId: string
|
issueId: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${bridgeId}/`
|
|
||||||
)
|
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeIssuesFromModuleBulk(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string,
|
||||||
|
issueIds: string[]
|
||||||
|
): Promise<any> {
|
||||||
|
const promiseDataUrls: any = [];
|
||||||
|
issueIds.forEach((issueId) => {
|
||||||
|
promiseDataUrls.push(
|
||||||
|
this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return await Promise.all(promiseDataUrls)
|
||||||
|
.then((response) => response)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeModulesFromIssueBulk(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleIds: string[]
|
||||||
|
): Promise<any> {
|
||||||
|
const promiseDataUrls: any = [];
|
||||||
|
moduleIds.forEach((moduleId) => {
|
||||||
|
promiseDataUrls.push(
|
||||||
|
this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return await Promise.all(promiseDataUrls)
|
||||||
|
.then((response) => response)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createModuleLink(
|
async createModuleLink(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -12,13 +12,14 @@ export interface IIssueStoreActions {
|
|||||||
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<TIssue>;
|
||||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
||||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<any>;
|
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
|
||||||
removeIssueFromModule: (
|
removeModulesFromIssue: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
issueId: string,
|
||||||
issueId: string
|
moduleIds: string[]
|
||||||
) => Promise<TIssue>;
|
) => Promise<void>;
|
||||||
|
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueStore extends IIssueStoreActions {
|
export interface IIssueStore extends IIssueStoreActions {
|
||||||
@ -143,15 +144,26 @@ export class IssueStore implements IIssueStore {
|
|||||||
return cycle;
|
return cycle;
|
||||||
};
|
};
|
||||||
|
|
||||||
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||||
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.addIssueToModule(
|
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.addModulesToIssue(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
moduleId,
|
issueId,
|
||||||
issueIds
|
moduleIds
|
||||||
);
|
);
|
||||||
if (issueIds && issueIds.length > 0)
|
if (moduleIds && moduleIds.length > 0)
|
||||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
|
return _module;
|
||||||
|
};
|
||||||
|
|
||||||
|
removeModulesFromIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||||
|
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeModulesFromIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
moduleIds
|
||||||
|
);
|
||||||
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return _module;
|
return _module;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -143,8 +143,10 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
||||||
this.issue.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
this.issue.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||||
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) =>
|
addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) =>
|
||||||
this.issue.addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
this.issue.addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
|
removeModulesFromIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) =>
|
||||||
|
this.issue.removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) =>
|
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) =>
|
||||||
this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||||
|
|
||||||
|
@ -52,13 +52,21 @@ export interface IModuleIssues {
|
|||||||
data: TIssue,
|
data: TIssue,
|
||||||
moduleId?: string | undefined
|
moduleId?: string | undefined
|
||||||
) => Promise<TIssue | undefined>;
|
) => Promise<TIssue | undefined>;
|
||||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<any>;
|
addIssuesToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||||
removeIssueFromModule: (
|
removeIssuesFromModule: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
issueId: string
|
issueIds: string[]
|
||||||
) => Promise<TIssue>;
|
) => Promise<void>;
|
||||||
|
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||||
|
removeModulesFromIssue: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleIds: string[]
|
||||||
|
) => Promise<void>;
|
||||||
|
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||||
@ -90,7 +98,10 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
updateIssue: action,
|
updateIssue: action,
|
||||||
removeIssue: action,
|
removeIssue: action,
|
||||||
quickAddIssue: action,
|
quickAddIssue: action,
|
||||||
addIssueToModule: action,
|
addIssuesToModule: action,
|
||||||
|
removeIssuesFromModule: action,
|
||||||
|
addModulesToIssue: action,
|
||||||
|
removeModulesFromIssue: action,
|
||||||
removeIssueFromModule: action,
|
removeIssueFromModule: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,7 +186,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.addIssueToModule(workspaceSlug, projectId, moduleId, [response.id]);
|
await this.addIssuesToModule(workspaceSlug, projectId, moduleId, [response.id]);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -253,7 +264,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
addIssuesToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
const issueToModule = await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, {
|
const issueToModule = await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, {
|
||||||
issues: issueIds,
|
issues: issueIds,
|
||||||
@ -261,11 +272,16 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
||||||
uniq(concat(moduleIssueIds, issueIds));
|
if (!moduleIssueIds) return [...issueIds];
|
||||||
|
else return uniq(concat(moduleIssueIds, issueIds));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
issueIds.forEach((issueId) => {
|
issueIds.forEach((issueId) => {
|
||||||
this.rootStore.issues.updateIssue(issueId, { module_id: moduleId });
|
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => {
|
||||||
|
if (issueModuleIds.includes(moduleId)) return issueModuleIds;
|
||||||
|
else return uniq(concat(issueModuleIds, [moduleId]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return issueToModule;
|
return issueToModule;
|
||||||
@ -274,14 +290,96 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
removeIssuesFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
issueIds.forEach((issueId) => {
|
||||||
|
pull(this.issues[moduleId], issueId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
issueIds.forEach((issueId) => {
|
||||||
|
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => {
|
||||||
|
if (issueModuleIds.includes(moduleId)) return pull(issueModuleIds, moduleId);
|
||||||
|
else return uniq(concat(issueModuleIds, [moduleId]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.moduleService.removeIssuesFromModuleBulk(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
moduleId,
|
||||||
|
issueIds
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||||
|
try {
|
||||||
|
const issueToModule = await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
|
||||||
|
modules: moduleIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
moduleIds.forEach((moduleId) => {
|
||||||
|
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
||||||
|
if (moduleIssueIds.includes(issueId)) return moduleIssueIds;
|
||||||
|
else return uniq(concat(moduleIssueIds, [issueId]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
|
||||||
|
uniq(concat(issueModuleIds, moduleIds))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return issueToModule;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
removeModulesFromIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
moduleIds.forEach((moduleId) => {
|
||||||
|
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
||||||
|
if (moduleIssueIds.includes(issueId)) return moduleIssueIds;
|
||||||
|
else return uniq(concat(moduleIssueIds, [issueId]));
|
||||||
|
});
|
||||||
|
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
|
||||||
|
pull(issueModuleIds, moduleId)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.moduleService.removeModulesFromIssueBulk(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
moduleIds
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
pull(this.issues[moduleId], issueId);
|
pull(this.issues[moduleId], issueId);
|
||||||
|
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
|
||||||
|
pull(issueModuleIds, moduleId)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore.issues.updateIssue(issueId, { module_id: null });
|
|
||||||
|
|
||||||
const response = await this.moduleService.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
const response = await this.moduleService.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
Loading…
Reference in New Issue
Block a user