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)
|
||||
parent_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
|
||||
label_ids = serializers.PrimaryKeyRelatedField(
|
||||
@ -597,7 +597,7 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
@ -613,6 +613,10 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||
]
|
||||
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):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
|
@ -35,17 +35,26 @@ urlpatterns = [
|
||||
name="project-modules",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"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",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-module-issues",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"get": "retrieve",
|
||||
|
@ -599,16 +599,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.order_by(order_by)
|
||||
.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()
|
||||
|
@ -100,7 +100,7 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_relation",
|
||||
@ -110,7 +110,6 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
)
|
||||
)
|
||||
.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()
|
||||
@ -221,9 +220,8 @@ def dashboard_created_issues(self, request, slug):
|
||||
)
|
||||
.filter(**filters)
|
||||
.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(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
|
@ -95,7 +95,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("labels", "assignees")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_inbox",
|
||||
@ -105,7 +105,6 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.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()
|
||||
|
@ -112,12 +112,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
@ -125,7 +121,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
.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()
|
||||
@ -1087,12 +1082,31 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.filter(archived_at__isnull=False)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@ -1120,22 +1134,6 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.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
|
||||
@ -1681,18 +1679,37 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
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)
|
||||
@ -1719,22 +1736,6 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.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
|
||||
|
@ -7,6 +7,8 @@ from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||
from django.core import serializers
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@ -296,23 +298,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"module_id": str(pk),
|
||||
"module_name": str(module.name),
|
||||
"issues": [str(issue_id) for issue_id in module_issues],
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
_ = [
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps({"module_id": str(pk)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue),
|
||||
project_id=project_id,
|
||||
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in module_issues
|
||||
]
|
||||
module.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -332,62 +331,18 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
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 list(self, request, slug, project_id, module_id):
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
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")
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.objects.filter(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
issue_module__module_id=self.kwargs.get("module_id")
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.order_by(order_by)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("labels", "assignees")
|
||||
.prefetch_related('issue_module__module')
|
||||
.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()
|
||||
@ -403,105 +358,118 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=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(
|
||||
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)
|
||||
|
||||
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", [])
|
||||
if not len(issues):
|
||||
return Response(
|
||||
{"error": "Issues are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||
)
|
||||
|
||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
||||
|
||||
update_module_issue_activity = []
|
||||
records_to_update = []
|
||||
record_to_create = []
|
||||
|
||||
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,
|
||||
)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
ModuleIssue(
|
||||
issue_id=str(issue),
|
||||
module_id=module_id,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
|
||||
ModuleIssue.objects.bulk_create(
|
||||
record_to_create,
|
||||
for issue in issues
|
||||
],
|
||||
batch_size=10,
|
||||
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(
|
||||
records_to_update,
|
||||
["module"],
|
||||
# create multiple module inside an issue
|
||||
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||
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,
|
||||
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_activity.delay(
|
||||
type="module.activity.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"),
|
||||
)
|
||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||
serializer = IssueSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
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):
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
@ -512,16 +480,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"module_id": str(module_id),
|
||||
"issues": [str(issue_id)],
|
||||
}
|
||||
),
|
||||
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
current_instance=json.dumps({"module_name": module_issue.module.name}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
|
@ -228,7 +228,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
parent = request.query_params.get("parent", "false")
|
||||
issue_relation = request.query_params.get("issue_relation", "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")
|
||||
|
||||
issue_id = request.query_params.get("issue_id", False)
|
||||
@ -269,8 +269,8 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
if cycle == "true":
|
||||
issues = issues.exclude(issue_cycle__isnull=False)
|
||||
|
||||
if module == "true":
|
||||
issues = issues.exclude(issue_module__isnull=False)
|
||||
if module:
|
||||
issues = issues.exclude(issue_module__module=module)
|
||||
|
||||
return Response(
|
||||
issues.values(
|
||||
|
@ -87,12 +87,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
@ -127,7 +123,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.filter(**filters)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.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()
|
||||
@ -150,13 +145,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
|
@ -1346,9 +1346,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
.filter(**filters)
|
||||
.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(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
|
@ -30,6 +30,7 @@ from plane.app.serializers import IssueActivitySerializer
|
||||
from plane.bgtasks.notification_task import notifications
|
||||
from plane.settings.redis import redis_instance
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
def track_name(
|
||||
requested_data,
|
||||
@ -852,70 +853,26 @@ def create_module_issue_activity(
|
||||
requested_data = (
|
||||
json.loads(requested_data) if requested_data is not None else None
|
||||
)
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_module_issues", [])
|
||||
created_records = json.loads(
|
||||
current_instance.get("created_module_issues", [])
|
||||
)
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_module = Module.objects.filter(
|
||||
pk=updated_record.get("old_module_id", None)
|
||||
).first()
|
||||
new_module = Module.objects.filter(
|
||||
pk=updated_record.get("new_module_id", None)
|
||||
).first()
|
||||
issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first()
|
||||
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,
|
||||
)
|
||||
module = Module.objects.filter(pk=requested_data.get("module_id")).first()
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
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=requested_data.get("module_id"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_module_issue_activity(
|
||||
@ -934,32 +891,26 @@ def delete_module_issue_activity(
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
module_id = requested_data.get("module_id", "")
|
||||
module_name = requested_data.get("module_name", "")
|
||||
module = Module.objects.filter(pk=module_id).first()
|
||||
issues = requested_data.get("issues")
|
||||
|
||||
for issue in issues:
|
||||
current_issue = Issue.objects.filter(pk=issue).first()
|
||||
if issue:
|
||||
current_issue.updated_at = timezone.now()
|
||||
current_issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue,
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=module.name if module is not None else module_name,
|
||||
new_value="",
|
||||
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,
|
||||
)
|
||||
module_name = current_instance.get("module_name")
|
||||
current_issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if current_issue:
|
||||
current_issue.updated_at = timezone.now()
|
||||
current_issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=module_name,
|
||||
new_value="",
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"removed this issue from {module_name}",
|
||||
old_identifier=requested_data.get("module_id") if requested_data.get("module_id") is not None else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_link_activity(
|
||||
@ -1648,7 +1599,6 @@ def issue_activity(
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
if notification:
|
||||
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(
|
||||
"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"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "module"]
|
||||
verbose_name = "Module Issue"
|
||||
verbose_name_plural = "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;
|
||||
parent_id: string | null;
|
||||
cycle_id: string | null;
|
||||
module_id: string | null;
|
||||
module_ids: string[] | null;
|
||||
|
||||
created_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;
|
||||
issue_relation?: boolean;
|
||||
cycle?: boolean;
|
||||
module?: boolean;
|
||||
module?: string[];
|
||||
sub_issue?: boolean;
|
||||
issue_id?: string;
|
||||
workspace_search: boolean;
|
||||
|
@ -216,7 +216,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isCreateIssueModalOpen}
|
||||
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}
|
||||
/>
|
||||
|
||||
|
@ -3,6 +3,7 @@ export * from "./cycle";
|
||||
export * from "./date";
|
||||
export * from "./estimate";
|
||||
export * from "./module";
|
||||
export * from "./module-select";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
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,
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
ModuleDropdown,
|
||||
ModuleSelectDropdown,
|
||||
PriorityDropdown,
|
||||
ProjectDropdown,
|
||||
ProjectMemberDropdown,
|
||||
@ -152,7 +152,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
project_id: watch("project_id"),
|
||||
parent_id: watch("parent_id"),
|
||||
cycle_id: watch("cycle_id"),
|
||||
module_id: watch("module_id"),
|
||||
module_ids: watch("module_ids"),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -570,15 +570,17 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{projectDetails?.module_view && (
|
||||
|
||||
{projectDetails?.module_view && workspaceSlug && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="module_id"
|
||||
name="module_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
<ModuleSelectDropdown
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId}
|
||||
value={value}
|
||||
value={value || undefined}
|
||||
onChange={(moduleId) => onChange(moduleId)}
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
@ -586,6 +588,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
||||
areEstimatesEnabledForProject(projectId) && (
|
||||
<Controller
|
||||
|
@ -94,7 +94,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
cycle: cycleId.toString(),
|
||||
}));
|
||||
}
|
||||
if (moduleId && !prePopulateDataProps?.module_id) {
|
||||
if (moduleId && !prePopulateDataProps?.module_ids) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
@ -123,7 +123,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
cycle: cycleId.toString(),
|
||||
}));
|
||||
}
|
||||
if (moduleId && !prePopulateDataProps?.module_id) {
|
||||
if (moduleId && !prePopulateDataProps?.module_ids) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...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;
|
||||
|
||||
await moduleService.addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, {
|
||||
issues: [issueId],
|
||||
await moduleService.addModulesToIssue(workspaceSlug as string, activeProject ?? "", issueId as string, {
|
||||
modules: moduleIds,
|
||||
});
|
||||
};
|
||||
|
||||
@ -248,7 +248,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
||||
.then(async (res) => {
|
||||
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({
|
||||
type: "success",
|
||||
|
@ -59,7 +59,7 @@ export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) =>
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import xor from "lodash/xor";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { ModuleDropdown } from "components/dropdowns";
|
||||
import { ModuleSelectDropdown } from "components/dropdowns";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
@ -32,58 +33,56 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
||||
const issue = getIssueById(issueId);
|
||||
const disableSelect = disabled || isUpdating;
|
||||
|
||||
const handleIssueModuleChange = async (moduleId: string | null) => {
|
||||
if (!issue || issue.module_id === moduleId) return;
|
||||
const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => {
|
||||
if (!issue) return;
|
||||
|
||||
setIsUpdating(true);
|
||||
if (moduleId) await issueOperations.addIssueToModule?.(workspaceSlug, projectId, moduleId, [issueId]);
|
||||
else await issueOperations.removeIssueFromModule?.(workspaceSlug, projectId, issue.module_id ?? "", issueId);
|
||||
if (moduleIds === undefined && issue?.module_ids && issue?.module_ids.length > 0)
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 h-full", className)}>
|
||||
<ModuleDropdown
|
||||
value={issue?.module_id ?? null}
|
||||
onChange={handleIssueModuleChange}
|
||||
buttonVariant="transparent-with-text"
|
||||
<div className={cn(`flex items-center gap-1 h-full`, className)}>
|
||||
<ModuleSelectDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
disabled={disableSelect}
|
||||
className="w-full group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.module_id ? "" : "text-custom-text-400"}`}
|
||||
value={issue?.module_ids?.length ? issue?.module_ids : undefined}
|
||||
onChange={handleIssueModuleChange}
|
||||
multiple={true}
|
||||
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}
|
||||
/> */}
|
||||
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" />}
|
||||
</div>
|
||||
);
|
||||
|
@ -29,13 +29,19 @@ export type TIssueOperations = {
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: 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>;
|
||||
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>;
|
||||
removeModulesFromIssue?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export type TIssueDetailRoot = {
|
||||
@ -57,8 +63,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||
removeIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addIssueToModule,
|
||||
addModulesToIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
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 {
|
||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||
await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
setToastAlert({
|
||||
title: "Module added to issue successfully",
|
||||
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,
|
||||
@ -191,8 +219,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||
removeArchivedIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addIssueToModule,
|
||||
addModulesToIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
setToastAlert,
|
||||
]
|
||||
);
|
||||
|
@ -286,7 +286,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
{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">
|
||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Module</span>
|
||||
|
@ -46,11 +46,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
|
||||
const issueIds = data.map((i) => i.id);
|
||||
await issues
|
||||
.addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
})
|
||||
.addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@ -69,7 +65,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
projectId={projectId}
|
||||
isOpen={moduleIssuesListModal}
|
||||
handleClose={() => setModuleIssuesListModal(false)}
|
||||
searchParams={{ module: true }}
|
||||
searchParams={{ module: moduleId != undefined ? [moduleId.toString()] : [] }}
|
||||
handleOnSubmit={handleAddIssuesToModule}
|
||||
/>
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
|
@ -44,7 +44,7 @@ export interface IBaseKanBanLayout {
|
||||
showLoader?: boolean;
|
||||
viewId?: string;
|
||||
storeType?: TCreateModalStoreTypes;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
@ -53,7 +53,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
storeType={EIssuesStoreType.MODULE}
|
||||
addIssuesToView={(issueIds: string[]) => {
|
||||
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;
|
||||
storeType: TCreateModalStoreTypes;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ export const HeaderGroupByCard = observer(
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
@ -51,7 +51,7 @@ export const ModuleListLayout: React.FC = observer(() => {
|
||||
storeType={EIssuesStoreType.MODULE}
|
||||
addIssuesToView={(issueIds: string[]) => {
|
||||
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,
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
ModuleDropdown,
|
||||
ModuleSelectDropdown,
|
||||
PriorityDropdown,
|
||||
ProjectDropdown,
|
||||
ProjectMemberDropdown,
|
||||
@ -44,7 +44,7 @@ const defaultValues: Partial<TIssue> = {
|
||||
assignee_ids: [],
|
||||
label_ids: [],
|
||||
cycle_id: null,
|
||||
module_id: null,
|
||||
module_ids: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
};
|
||||
@ -541,21 +541,24 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{projectDetails?.module_view && (
|
||||
{projectDetails?.module_view && workspaceSlug && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="module_id"
|
||||
name="module_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
<ModuleSelectDropdown
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId}
|
||||
value={value}
|
||||
value={value || undefined}
|
||||
onChange={(moduleId) => {
|
||||
onChange(moduleId);
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={13}
|
||||
multiple={true}
|
||||
showCount={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -108,11 +108,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
fetchCycleDetails(workspaceSlug, activeProjectId, cycleId);
|
||||
};
|
||||
|
||||
const addIssueToModule = async (issue: TIssue, moduleId: string) => {
|
||||
const addIssueToModule = async (issue: TIssue, moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !activeProjectId) return;
|
||||
|
||||
await moduleIssues.addIssueToModule(workspaceSlug, activeProjectId, moduleId, [issue.id]);
|
||||
fetchModuleDetails(workspaceSlug, activeProjectId, moduleId);
|
||||
await moduleIssues.addModulesToIssue(workspaceSlug, activeProjectId, issue.id, moduleIds);
|
||||
moduleIds.forEach((moduleId) => fetchModuleDetails(workspaceSlug, activeProjectId, moduleId));
|
||||
};
|
||||
|
||||
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)
|
||||
await addIssueToCycle(response, payload.cycle_id);
|
||||
if (payload.module_id && payload.module_id !== "" && storeType !== EIssuesStoreType.MODULE)
|
||||
await addIssueToModule(response, payload.module_id);
|
||||
if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE)
|
||||
await addIssueToModule(response, payload.module_ids);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@ -278,7 +278,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
data={{
|
||||
...data,
|
||||
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}
|
||||
onClose={handleClose}
|
||||
@ -292,7 +292,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
data={{
|
||||
...data,
|
||||
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)}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
|
@ -203,7 +203,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
)}
|
||||
|
||||
{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">
|
||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Module</span>
|
||||
|
@ -28,8 +28,19 @@ export type TIssuePeekOperations = {
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: 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>;
|
||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: 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>;
|
||||
removeModulesFromIssue?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
@ -48,7 +59,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
removeIssue,
|
||||
issue: { getIssueById, fetchIssue },
|
||||
} = useIssueDetail();
|
||||
const { addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule } = useIssueDetail();
|
||||
const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } =
|
||||
useIssueDetail();
|
||||
// state
|
||||
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 {
|
||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||
await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
setToastAlert({
|
||||
title: "Module added to issue successfully",
|
||||
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,
|
||||
@ -184,8 +217,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
removeArchivedIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addIssueToModule,
|
||||
addModulesToIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
setToastAlert,
|
||||
onIssueUpdate,
|
||||
]
|
||||
|
@ -47,7 +47,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
<InstanceLayout>
|
||||
<StoreWrapper>
|
||||
<CrispWrapper user={currentUser}>
|
||||
<PosthogWrapper
|
||||
{/* <PosthogWrapper
|
||||
user={currentUser}
|
||||
workspaceRole={currentWorkspaceRole}
|
||||
projectRole={currentProjectRole}
|
||||
@ -55,7 +55,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
posthogHost={envConfig?.posthog_host || null}
|
||||
>
|
||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||
</PosthogWrapper>
|
||||
</PosthogWrapper> */}
|
||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||
</CrispWrapper>
|
||||
</StoreWrapper>
|
||||
</InstanceLayout>
|
||||
|
@ -1,7 +1,7 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// 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";
|
||||
|
||||
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[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-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/`, {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
@ -92,15 +77,21 @@ export class ModuleService extends APIService {
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
data: { issues: string[] }
|
||||
): Promise<
|
||||
{
|
||||
issue: string;
|
||||
issue_detail: TIssue;
|
||||
module: string;
|
||||
module_detail: IModule;
|
||||
}[]
|
||||
> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data)
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.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)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
@ -111,17 +102,53 @@ export class ModuleService extends APIService {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
bridgeId: string
|
||||
issueId: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${bridgeId}/`
|
||||
)
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
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(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -12,13 +12,14 @@ export interface IIssueStoreActions {
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: 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>;
|
||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<any>;
|
||||
removeIssueFromModule: (
|
||||
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
|
||||
removeModulesFromIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueId: string
|
||||
) => Promise<TIssue>;
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
) => Promise<void>;
|
||||
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IIssueStore extends IIssueStoreActions {
|
||||
@ -143,15 +144,26 @@ export class IssueStore implements IIssueStore {
|
||||
return cycle;
|
||||
};
|
||||
|
||||
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.addIssueToModule(
|
||||
addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.addModulesToIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
moduleId,
|
||||
issueIds
|
||||
issueId,
|
||||
moduleIds
|
||||
);
|
||||
if (issueIds && issueIds.length > 0)
|
||||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
||||
if (moduleIds && moduleIds.length > 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;
|
||||
};
|
||||
|
||||
|
@ -143,8 +143,10 @@ export class IssueDetail implements IIssueDetail {
|
||||
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
||||
this.issue.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) =>
|
||||
this.issue.addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||
addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) =>
|
||||
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) =>
|
||||
this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||
|
||||
|
@ -52,13 +52,21 @@ export interface IModuleIssues {
|
||||
data: TIssue,
|
||||
moduleId?: string | undefined
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<any>;
|
||||
removeIssueFromModule: (
|
||||
addIssuesToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssuesFromModule: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueId: string
|
||||
) => Promise<TIssue>;
|
||||
issueIds: string[]
|
||||
) => 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 {
|
||||
@ -90,7 +98,10 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
quickAddIssue: action,
|
||||
addIssueToModule: action,
|
||||
addIssuesToModule: action,
|
||||
removeIssuesFromModule: action,
|
||||
addModulesToIssue: action,
|
||||
removeModulesFromIssue: action,
|
||||
removeIssueFromModule: action,
|
||||
});
|
||||
|
||||
@ -175,7 +186,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
if (!moduleId) throw new Error("Module Id is required");
|
||||
|
||||
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;
|
||||
} 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 {
|
||||
const issueToModule = await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, {
|
||||
issues: issueIds,
|
||||
@ -261,11 +272,16 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
|
||||
runInAction(() => {
|
||||
update(this.issues, moduleId, (moduleIssueIds = []) => {
|
||||
uniq(concat(moduleIssueIds, issueIds));
|
||||
if (!moduleIssueIds) return [...issueIds];
|
||||
else return uniq(concat(moduleIssueIds, issueIds));
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
@ -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) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
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);
|
||||
|
||||
return response;
|
||||
|
Loading…
Reference in New Issue
Block a user