Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 96 additions & 39 deletions apps/api/plane/api/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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": [],
},
Comment on lines 2291 to 2323
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Propagate this response-shape change end-to-end.

list_work_item_relations now returns relation refs ({project_id, issue_id}), but apps/api/plane/api/serializers/issue.py still documents these fields as ListField(UUIDField()), and apps/web/core/store/issue/issue-details/relation.store.ts:115-133 still consumes issue.id and passes each item into addIssue(). As-is, this ships the wrong OpenAPI contract and will break the current relations UI unless the web consumer/service is updated in the same PR or the old id-based shape is preserved.

Also applies to: 2337-2413

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/issue.py` around lines 2291 - 2323, The API changed:
list_work_item_relations now returns relation refs ({project_id, issue_id}) but
the serializer and web consumer still expect flat UUID lists and issue objects;
update apps/api/plane/api/serializers/issue.py to document the new shape
(replace the ListField(UUIDField()) relation fields with a ListField of a
dict/nested serializer containing "project_id" and "issue_id") so the OpenAPI
contract matches list_work_item_relations, and update the consumer in
apps/web/core/store/issue/issue-details/relation.store.ts (the code that
currently iterates and calls addIssue(item) around lines 115-133) to either
extract and pass the correct issue id (item.issue_id) to addIssue or adapt
addIssue to accept the {project_id, issue_id} ref; make the serializer and the
web store consistent with list_work_item_relations across all relation fields
(blocking, blocked_by, duplicate, relates_to, start_after, start_before,
finish_after, finish_before).

Expand All @@ -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)

Expand Down
Loading