[MOBIL-168] issue relations (#1206)

* chore: issue relation query

* chore: import error

* chore: blocking issues list update

* chore: updated search and remove in issue relation

* chore: updated add and remove relation

* chore: created issue comment mutation

* chore: replaced the project filter with workspace filter
This commit is contained in:
guru_sainath
2024-09-24 14:16:08 +05:30
committed by GitHub
parent ad924f296a
commit 57aff09fbb
9 changed files with 375 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from .relation import IssueRelationMutation
from .comment import IssueCommentMutation

View File

@@ -0,0 +1,47 @@
# Strawberry imports
import strawberry
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Third-party imports
from asgiref.sync import sync_to_async
# Module imports
from plane.graphql.permissions.project import ProjectBasePermission
from plane.db.models import Workspace, IssueComment
@strawberry.type
class IssueCommentMutation:
# adding issue comment
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def addIssueComment(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
comment_html: str,
) -> bool:
workspace_details = await sync_to_async(
Workspace.objects.filter(slug=slug).first
)()
if not workspace_details:
return False
await sync_to_async(
lambda: IssueComment.objects.create(
workspace_id=workspace_details.id,
project_id=project,
issue_id=issue,
comment_html=comment_html,
actor=info.context.user,
created_by=info.context.user,
updated_by=info.context.user,
)
)()
return True

View File

@@ -0,0 +1,98 @@
# Strawberry imports
import strawberry
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Third-party imports
from asgiref.sync import sync_to_async
# Module imports
from plane.graphql.permissions.project import ProjectBasePermission
from plane.db.models import Workspace, IssueRelation
@strawberry.type
class IssueRelationMutation:
# adding issue relation
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def addIssueRelation(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
relation_type: str,
related_issue_ids: list[strawberry.ID],
) -> bool:
workspace_details = await sync_to_async(
Workspace.objects.filter(slug=slug).first
)()
if not workspace_details:
return False
issue_relations = [
IssueRelation(
issue_id=(
related_issue_id if relation_type == "blocking" else issue
),
related_issue_id=(
issue if relation_type == "blocking" else related_issue_id
),
relation_type=(
"blocked_by"
if relation_type == "blocking"
else relation_type
),
project_id=project,
workspace_id=workspace_details.id,
created_by=info.context.user,
updated_by=info.context.user,
)
for related_issue_id in related_issue_ids
]
await sync_to_async(
lambda: IssueRelation.objects.bulk_create(
issue_relations,
batch_size=10,
ignore_conflicts=True,
)
)()
return True
# removing issue relation
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def removeIssueRelation(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
relation_type: str,
related_issue: strawberry.ID,
) -> bool:
issue_relation = await sync_to_async(
lambda: IssueRelation.objects.get(
workspace__slug=slug,
project_id=project,
issue_id=(
related_issue if relation_type == "blocking" else issue
),
related_issue_id=(
issue if relation_type == "blocking" else related_issue
),
)
)()
if not issue_relation:
return False
await sync_to_async(lambda: issue_relation.delete())()
return True

View File

@@ -0,0 +1,2 @@
from .relation import IssueRelationQuery
from .search import IssuesSearchQuery

View File

@@ -0,0 +1,96 @@
# Third-Party Imports
import strawberry
# Python Standard Library Imports
from asgiref.sync import sync_to_async
# Django Imports
from django.db.models import Q, Value, CharField
# Strawberry Imports
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Module Imports
from plane.graphql.permissions.project import ProjectBasePermission
from plane.db.models import IssueRelation, Issue
from plane.graphql.types.issues.relation import IssueRelationType
@strawberry.type
class IssueRelationQuery:
# getting issue relation issues
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def issueRelation(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
) -> IssueRelationType:
# construct the issue relation query
issue_relation_query = (
IssueRelation.objects.filter(workspace__slug=slug)
.filter(project__id=project)
.filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
.filter(Q(issue_id=issue) | Q(related_issue=issue))
.select_related("project")
.select_related("workspace")
.select_related("issue")
.order_by("-created_at")
.distinct()
)
# getting all blocking issues
blocking_issue_ids = await sync_to_async(list)(
issue_relation_query.filter(
relation_type="blocked_by", related_issue_id=issue
).values_list("issue_id", flat=True)
)
# getting all blocked by issues
blocked_by_issue_ids = await sync_to_async(list)(
issue_relation_query.filter(
relation_type="blocked_by", issue_id=issue
).values_list("related_issue_id", flat=True)
)
# constructing the issue query
issue_queryset = (
Issue.issue_objects.filter(
workspace__slug=slug, project_id=project
)
.filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
)
# getting all blocking issues
blocking_issues = await sync_to_async(list)(
issue_queryset.filter(id__in=blocking_issue_ids).annotate(
relation_type=Value("blocking", output_field=CharField())
)
)
# getting all blocked by issues
blocked_by_issues = await sync_to_async(list)(
issue_queryset.filter(id__in=blocked_by_issue_ids).annotate(
relation_type=Value("blocked_by", output_field=CharField())
)
)
relation_response = IssueRelationType(
blocking=blocking_issues,
blocked_by=blocked_by_issues,
)
return relation_response

View File

@@ -0,0 +1,112 @@
# Python imports
import re
# Third-Party Imports
import strawberry
from typing import Optional
# Python Standard Library Imports
from asgiref.sync import sync_to_async
# Django Imports
from django.db.models import Q
# Strawberry Imports
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Module Imports
from plane.graphql.permissions.workspace import WorkspaceBasePermission
from plane.db.models import Issue
from plane.graphql.types.issue import IssueLiteType
@strawberry.type
class IssuesSearchQuery:
# getting issues which are not related
@strawberry.field(
extensions=[
PermissionExtension(permissions=[WorkspaceBasePermission()])
]
)
# getting issue relation issues
async def issuesSearch(
self,
info: Info,
slug: str,
project: Optional[strawberry.ID] = None,
module: Optional[strawberry.ID] = None,
cycle: Optional[strawberry.ID] = None,
issue: Optional[strawberry.ID] = None,
relation_type: Optional[bool] = False,
search: Optional[str] = None,
) -> list[IssueLiteType]:
issue_queryset = Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
# workspace issues
if project:
issue_queryset = issue_queryset.filter(project_id=project)
# module issues
if module:
issue_queryset = issue_queryset.filter(
issue_module__module_id=module
)
# cycle issues
if cycle:
issue_queryset = issue_queryset.filter(issue_cycle__cycle_id=cycle)
# issue relation issues
if relation_type and issue:
issue_queryset = issue_queryset.filter(
~Q(pk=issue),
~Q(
issue_related__issue=issue,
issue_related__deleted_at__isnull=True,
),
~Q(
issue_relation__related_issue=issue,
issue_related__deleted_at__isnull=True,
),
)
# apply search filter
q = Q()
if search:
fields = ["name", "sequence_id", "project__identifier"]
for field in fields:
if field == "sequence_id":
# Match whole integers only (exclude decimal numbers)
sequences = re.findall(r"\b\d+\b", search)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": search})
issues = await sync_to_async(
lambda: list(
issue_queryset.filter(q)
.distinct()
.values(
"id",
"sequence_id",
"name",
"project",
"project__identifier",
)
)
)()
for issue in issues:
issue["project_identifier"] = issue["project__identifier"]
del issue["project__identifier"]
return [IssueLiteType(**issue) for issue in issues]

View File

@@ -42,6 +42,7 @@ from .queries.search import GlobalSearchQuery
from .queries.attachment import IssueAttachmentQuery
from .queries.link import IssueLinkQuery
from .queries.estimate import EstimatePointQuery
from .queries.issues import IssueRelationQuery, IssuesSearchQuery
# mutations
from .mutations.workspace import WorkspaceMutation, WorkspaceInviteMutation
@@ -71,6 +72,7 @@ from .mutations.module import (
)
from .mutations.link import IssueLinkMutation
from .mutations.favorite import UserFavoriteMutation
from .mutations.issues import IssueRelationMutation, IssueCommentMutation
# combined query class for all
@@ -114,6 +116,8 @@ class Query(
UserPageQuery,
CycleIssueUserPropertyQuery,
ModuleIssueUserPropertyQuery,
IssueRelationQuery,
IssuesSearchQuery,
):
pass
@@ -141,6 +145,8 @@ class Mutation(
UserFavoriteMutation,
CycleIssueUserPropertyMutation,
ModuleIssueUserPropertyMutation,
IssueRelationMutation,
IssueCommentMutation,
):
pass

View File

@@ -0,0 +1,12 @@
# Strawberry imports
import strawberry_django
# Module Imports
from plane.db.models import IssueRelation
from plane.graphql.types.issue import IssuesType
@strawberry_django.type(IssueRelation)
class IssueRelationType:
blocking: list[IssuesType]
blocked_by: list[IssuesType]