diff --git a/apps/api/plane/api/serializers/issue.py b/apps/api/plane/api/serializers/issue.py index 6468ddbc84f..048de6e92b1 100644 --- a/apps/api/plane/api/serializers/issue.py +++ b/apps/api/plane/api/serializers/issue.py @@ -480,44 +480,52 @@ class Meta: ] +class IssueRelationRefSerializer(serializers.Serializer): + """Project-scoped reference to a related work item.""" + + project_id = serializers.UUIDField(help_text="Project containing the related work item") + issue_id = serializers.UUIDField(help_text="ID of the related work item") + + class IssueRelationResponseSerializer(serializers.Serializer): """ Serializer for issue relations response showing grouped relation types. - Returns issue IDs organized by relation type for efficient client-side processing. + Each list contains project_id and issue_id pairs so clients can resolve + cross-project relations. """ blocking = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that are blocking this issue", + child=IssueRelationRefSerializer(), + help_text="Work items blocking this issue", ) blocked_by = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that this issue is blocked by", + child=IssueRelationRefSerializer(), + help_text="Work items this issue is blocked by", ) duplicate = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that are duplicates of this issue", + child=IssueRelationRefSerializer(), + help_text="Duplicate work items", ) relates_to = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that relate to this issue", + child=IssueRelationRefSerializer(), + help_text="Related work items", ) start_after = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that start after this issue", + child=IssueRelationRefSerializer(), + help_text="Work items that start after this issue", ) start_before = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that start before this issue", + child=IssueRelationRefSerializer(), + help_text="Work items that start before this issue", ) finish_after = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that finish after this issue", + child=IssueRelationRefSerializer(), + help_text="Work items that finish after this issue", ) finish_before = serializers.ListField( - child=serializers.UUIDField(), - help_text="List of issue IDs that finish before this issue", + child=IssueRelationRefSerializer(), + help_text="Work items that finish before this issue", ) diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index 97e8e7cee0a..180d6844754 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -23,11 +23,8 @@ Value, When, Subquery, - UUIDField, ) -from django.db.models.functions import Coalesce -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField + from django.utils import timezone from django.conf import settings @@ -2292,14 +2289,35 @@ class IssueRelationListCreateAPIEndpoint(BaseAPIView): name="Work Item Relations Response", value={ "blocking": [ - "550e8400-e29b-41d4-a716-446655440000", - "550e8400-e29b-41d4-a716-446655440001", + { + "project_id": "550e8400-e29b-41d4-a716-446655440010", + "issue_id": "550e8400-e29b-41d4-a716-446655440000", + }, + { + "project_id": "550e8400-e29b-41d4-a716-446655440010", + "issue_id": "550e8400-e29b-41d4-a716-446655440001", + }, + ], + "blocked_by": [ + { + "project_id": "550e8400-e29b-41d4-a716-446655440011", + "issue_id": "550e8400-e29b-41d4-a716-446655440002", + }, ], - "blocked_by": ["550e8400-e29b-41d4-a716-446655440002"], "duplicate": [], - "relates_to": ["550e8400-e29b-41d4-a716-446655440003"], + "relates_to": [ + { + "project_id": "550e8400-e29b-41d4-a716-446655440010", + "issue_id": "550e8400-e29b-41d4-a716-446655440003", + }, + ], "start_after": [], - "start_before": ["550e8400-e29b-41d4-a716-446655440004"], + "start_before": [ + { + "project_id": "550e8400-e29b-41d4-a716-446655440012", + "issue_id": "550e8400-e29b-41d4-a716-446655440004", + }, + ], "finish_after": [], "finish_before": [], }, @@ -2316,42 +2334,81 @@ def get(self, request, slug, project_id, issue_id): Retrieve all relationships for a work item organized by relation type. Returns a structured response with relations grouped by type. """ - empty_uuid_array = Value([], output_field=ArrayField(UUIDField())) - - def _agg_ids(field, **filter_kwargs): - return Coalesce( - ArrayAgg(field, filter=Q(**filter_kwargs), distinct=True), - empty_uuid_array, - ) - - issue_relation_qs = IssueRelation.objects.filter( + relations = IssueRelation.objects.filter( Q(issue_id=issue_id) | Q(related_issue_id=issue_id), workspace__slug=slug, - ) - - relation_ids = issue_relation_qs.aggregate( - blocking_ids=_agg_ids("issue_id", relation_type="blocked_by", related_issue_id=issue_id), - blocked_by_ids=_agg_ids("related_issue_id", relation_type="blocked_by", issue_id=issue_id), - duplicate_ids=_agg_ids("related_issue_id", relation_type="duplicate", issue_id=issue_id), - duplicate_ids_related=_agg_ids("issue_id", relation_type="duplicate", related_issue_id=issue_id), - relates_to_ids=_agg_ids("related_issue_id", relation_type="relates_to", issue_id=issue_id), - relates_to_ids_related=_agg_ids("issue_id", relation_type="relates_to", related_issue_id=issue_id), - start_after_ids=_agg_ids("issue_id", relation_type="start_before", related_issue_id=issue_id), - start_before_ids=_agg_ids("related_issue_id", relation_type="start_before", issue_id=issue_id), - finish_after_ids=_agg_ids("issue_id", relation_type="finish_before", related_issue_id=issue_id), - finish_before_ids=_agg_ids("related_issue_id", relation_type="finish_before", issue_id=issue_id), + ).values( + "relation_type", + "issue_id", + "related_issue_id", + issue_project_id=F("issue__project_id"), + related_issue_project_id=F("related_issue__project_id"), ) response_data = { - "blocking": relation_ids["blocking_ids"], - "blocked_by": relation_ids["blocked_by_ids"], - "duplicate": list(set(relation_ids["duplicate_ids"] + relation_ids["duplicate_ids_related"])), - "relates_to": list(set(relation_ids["relates_to_ids"] + relation_ids["relates_to_ids_related"])), - "start_after": relation_ids["start_after_ids"], - "start_before": relation_ids["start_before_ids"], - "finish_after": relation_ids["finish_after_ids"], - "finish_before": relation_ids["finish_before_ids"], + "blocking": [], + "blocked_by": [], + "duplicate": [], + "relates_to": [], + "start_after": [], + "start_before": [], + "finish_after": [], + "finish_before": [], } + seen_duplicate = set() + seen_relates_to = set() + + for rel in relations: + rt = rel["relation_type"] + if rt == "blocked_by": + if str(rel["related_issue_id"]) == str(issue_id): + response_data["blocking"].append( + {"project_id": str(rel["issue_project_id"]), "issue_id": str(rel["issue_id"])} + ) + if str(rel["issue_id"]) == str(issue_id): + response_data["blocked_by"].append( + {"project_id": str(rel["related_issue_project_id"]), "issue_id": str(rel["related_issue_id"])} + ) + elif rt == "duplicate": + if str(rel["issue_id"]) == str(issue_id) and rel["related_issue_id"] not in seen_duplicate: + seen_duplicate.add(rel["related_issue_id"]) + response_data["duplicate"].append( + {"project_id": str(rel["related_issue_project_id"]), "issue_id": str(rel["related_issue_id"])} + ) + if str(rel["related_issue_id"]) == str(issue_id) and rel["issue_id"] not in seen_duplicate: + seen_duplicate.add(rel["issue_id"]) + response_data["duplicate"].append( + {"project_id": str(rel["issue_project_id"]), "issue_id": str(rel["issue_id"])} + ) + elif rt == "relates_to": + if str(rel["issue_id"]) == str(issue_id) and rel["related_issue_id"] not in seen_relates_to: + seen_relates_to.add(rel["related_issue_id"]) + response_data["relates_to"].append( + {"project_id": str(rel["related_issue_project_id"]), "issue_id": str(rel["related_issue_id"])} + ) + if str(rel["related_issue_id"]) == str(issue_id) and rel["issue_id"] not in seen_relates_to: + seen_relates_to.add(rel["issue_id"]) + response_data["relates_to"].append( + {"project_id": str(rel["issue_project_id"]), "issue_id": str(rel["issue_id"])} + ) + elif rt == "start_before": + if str(rel["related_issue_id"]) == str(issue_id): + response_data["start_after"].append( + {"project_id": str(rel["issue_project_id"]), "issue_id": str(rel["issue_id"])} + ) + if str(rel["issue_id"]) == str(issue_id): + response_data["start_before"].append( + {"project_id": str(rel["related_issue_project_id"]), "issue_id": str(rel["related_issue_id"])} + ) + elif rt == "finish_before": + if str(rel["related_issue_id"]) == str(issue_id): + response_data["finish_after"].append( + {"project_id": str(rel["issue_project_id"]), "issue_id": str(rel["issue_id"])} + ) + if str(rel["issue_id"]) == str(issue_id): + response_data["finish_before"].append( + {"project_id": str(rel["related_issue_project_id"]), "issue_id": str(rel["related_issue_id"])} + ) return Response(response_data, status=status.HTTP_200_OK)