From 56c38dc52195bcffe47e5ea9741c3aaa311b8cbb Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Tue, 14 Apr 2026 01:30:40 +0800 Subject: [PATCH] fix(issue): inherit parent project for sub-issues --- server/internal/handler/handler_test.go | 67 +++++++++++++++++++++++++ server/internal/handler/issue.go | 9 +++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/server/internal/handler/handler_test.go b/server/internal/handler/handler_test.go index 449712a7ff..9a3c3dba11 100644 --- a/server/internal/handler/handler_test.go +++ b/server/internal/handler/handler_test.go @@ -302,6 +302,73 @@ func TestCreateIssueExplicitBacklogPreserved(t *testing.T) { testHandler.DeleteIssue(httptest.NewRecorder(), cleanupReq) } +func TestCreateSubIssueInheritsParentProject(t *testing.T) { + var projectID, parentID, childID string + defer func() { + for _, issueID := range []string{childID, parentID} { + if issueID == "" { + continue + } + req := newRequest("DELETE", "/api/issues/"+issueID, nil) + req = withURLParam(req, "id", issueID) + testHandler.DeleteIssue(httptest.NewRecorder(), req) + } + if projectID != "" { + req := newRequest("DELETE", "/api/projects/"+projectID, nil) + req = withURLParam(req, "id", projectID) + testHandler.DeleteProject(httptest.NewRecorder(), req) + } + }() + + w := httptest.NewRecorder() + req := newRequest("POST", "/api/projects?workspace_id="+testWorkspaceID, map[string]any{ + "title": "Sub-issue inheritance project", + }) + testHandler.CreateProject(w, req) + if w.Code != http.StatusCreated { + t.Fatalf("CreateProject: expected 201, got %d: %s", w.Code, w.Body.String()) + } + var project ProjectResponse + json.NewDecoder(w.Body).Decode(&project) + projectID = project.ID + + w = httptest.NewRecorder() + req = newRequest("POST", "/api/issues?workspace_id="+testWorkspaceID, map[string]any{ + "title": "Parent with project", + "project_id": projectID, + }) + testHandler.CreateIssue(w, req) + if w.Code != http.StatusCreated { + t.Fatalf("CreateIssue parent: expected 201, got %d: %s", w.Code, w.Body.String()) + } + var parent IssueResponse + json.NewDecoder(w.Body).Decode(&parent) + parentID = parent.ID + if parent.ProjectID == nil || *parent.ProjectID != projectID { + t.Fatalf("CreateIssue parent: expected project_id %q, got %v", projectID, parent.ProjectID) + } + + w = httptest.NewRecorder() + req = newRequest("POST", "/api/issues?workspace_id="+testWorkspaceID, map[string]any{ + "title": "Child without explicit project", + "parent_issue_id": parentID, + }) + testHandler.CreateIssue(w, req) + if w.Code != http.StatusCreated { + t.Fatalf("CreateIssue child: expected 201, got %d: %s", w.Code, w.Body.String()) + } + var child IssueResponse + json.NewDecoder(w.Body).Decode(&child) + childID = child.ID + + if child.ParentIssueID == nil || *child.ParentIssueID != parentID { + t.Fatalf("CreateIssue child: expected parent_issue_id %q, got %v", parentID, child.ParentIssueID) + } + if child.ProjectID == nil || *child.ProjectID != projectID { + t.Fatalf("CreateIssue child: expected inherited project_id %q, got %v", projectID, child.ProjectID) + } +} + func TestCommentCRUD(t *testing.T) { // Create an issue first w := httptest.NewRecorder() diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go index c5e5d1d73f..7a7049715c 100644 --- a/server/internal/handler/issue.go +++ b/server/internal/handler/issue.go @@ -815,6 +815,10 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) { } var parentIssueID pgtype.UUID + var projectID pgtype.UUID + if req.ProjectID != nil { + projectID = parseUUID(*req.ProjectID) + } if req.ParentIssueID != nil { parentIssueID = parseUUID(*req.ParentIssueID) // Validate parent exists in the same workspace. @@ -826,6 +830,9 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "parent issue not found in this workspace") return } + if req.ProjectID == nil { + projectID = parent.ProjectID + } } var dueDate pgtype.Timestamptz @@ -872,7 +879,7 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) { Position: 0, DueDate: dueDate, Number: issueNumber, - ProjectID: func() pgtype.UUID { if req.ProjectID != nil { return parseUUID(*req.ProjectID) }; return pgtype.UUID{} }(), + ProjectID: projectID, }) if err != nil { slog.Warn("create issue failed", append(logger.RequestAttrs(r), "error", err, "workspace_id", workspaceID)...)