From 083818cb85f87e3adb0952f25d8fb2c2107364e2 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 24 Jun 2023 23:31:28 +0800
Subject: [PATCH] Improve loadprojects for issue list (#25468)

---
 models/issues/issue_list.go                   | 38 ++++++++++---------
 models/issues/issue_list_test.go              |  4 ++
 models/issues/issue_project.go                |  5 ---
 routers/web/org/projects.go                   |  9 ++++-
 routers/web/repo/issue.go                     |  4 +-
 routers/web/repo/projects.go                  |  7 +++-
 .../repo/issue/view_content/sidebar.tmpl      |  4 +-
 7 files changed, 41 insertions(+), 30 deletions(-)

diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index dad21c1477..9cc41ec6ab 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -229,39 +229,41 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
 	return nil
 }
 
-func (issues IssueList) getProjectIDs() []int64 {
-	ids := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
-		ids.Add(issue.ProjectID())
-	}
-	return ids.Values()
-}
+func (issues IssueList) LoadProjects(ctx context.Context) error {
+	issueIDs := issues.getIssueIDs()
+	projectMaps := make(map[int64]*project_model.Project, len(issues))
+	left := len(issueIDs)
 
-func (issues IssueList) loadProjects(ctx context.Context) error {
-	projectIDs := issues.getProjectIDs()
-	if len(projectIDs) == 0 {
-		return nil
+	type projectWithIssueID struct {
+		*project_model.Project `xorm:"extends"`
+		IssueID                int64
 	}
 
-	projectMaps := make(map[int64]*project_model.Project, len(projectIDs))
-	left := len(projectIDs)
 	for left > 0 {
 		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
+
+		projects := make([]*projectWithIssueID, 0, limit)
 		err := db.GetEngine(ctx).
-			In("id", projectIDs[:limit]).
-			Find(&projectMaps)
+			Table("project").
+			Select("project.*, project_issue.issue_id").
+			Join("INNER", "project_issue", "project.id = project_issue.project_id").
+			In("project_issue.issue_id", issueIDs[:limit]).
+			Find(&projects)
 		if err != nil {
 			return err
 		}
+		for _, project := range projects {
+			projectMaps[project.IssueID] = project.Project
+		}
 		left -= limit
-		projectIDs = projectIDs[limit:]
+		issueIDs = issueIDs[limit:]
 	}
 
 	for _, issue := range issues {
-		issue.Project = projectMaps[issue.ProjectID()]
+		issue.Project = projectMaps[issue.ID]
 	}
 	return nil
 }
@@ -541,7 +543,7 @@ func (issues IssueList) loadAttributes(ctx context.Context) error {
 		return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
 	}
 
-	if err := issues.loadProjects(ctx); err != nil {
+	if err := issues.LoadProjects(ctx); err != nil {
 		return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
 	}
 
diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go
index 954a20ffe4..97ce9e43b3 100644
--- a/models/issues/issue_list_test.go
+++ b/models/issues/issue_list_test.go
@@ -66,8 +66,12 @@ func TestIssueList_LoadAttributes(t *testing.T) {
 		}
 		if issue.ID == int64(1) {
 			assert.Equal(t, int64(400), issue.TotalTrackedTime)
+			assert.NotNil(t, issue.Project)
 		} else if issue.ID == int64(2) {
 			assert.Equal(t, int64(3682), issue.TotalTrackedTime)
+			assert.Nil(t, issue.Project)
+		} else {
+			assert.Nil(t, issue.Project)
 		}
 	}
 }
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index 04d12e055c..b163c68357 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -27,11 +27,6 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) {
 	return err
 }
 
-// ProjectID return project id if issue was assigned to one
-func (issue *Issue) ProjectID() int64 {
-	return issue.projectID(db.DefaultContext)
-}
-
 func (issue *Issue) projectID(ctx context.Context) int64 {
 	var ip project_model.ProjectIssue
 	has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index b3f6024b60..e525f2c43f 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -383,7 +383,7 @@ func ViewProject(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplProjectsView)
 }
 
-func getActionIssues(ctx *context.Context) []*issues_model.Issue {
+func getActionIssues(ctx *context.Context) issues_model.IssueList {
 	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
 	if len(commaSeparatedIssueIDs) == 0 {
 		return nil
@@ -429,9 +429,14 @@ func UpdateIssueProject(ctx *context.Context) {
 		return
 	}
 
+	if err := issues.LoadProjects(ctx); err != nil {
+		ctx.ServerError("LoadProjects", err)
+		return
+	}
+
 	projectID := ctx.FormInt64("id")
 	for _, issue := range issues {
-		oldProjectID := issue.ProjectID()
+		oldProjectID := issue.Project.ID
 		if oldProjectID == projectID {
 			continue
 		}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 095738ff29..a0dd14e314 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1984,7 +1984,7 @@ func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
 	}
 }
 
-func getActionIssues(ctx *context.Context) []*issues_model.Issue {
+func getActionIssues(ctx *context.Context) issues_model.IssueList {
 	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
 	if len(commaSeparatedIssueIDs) == 0 {
 		return nil
@@ -2749,7 +2749,7 @@ func UpdateIssueStatus(ctx *context.Context) {
 		log.Warn("Unrecognized action: %s", action)
 	}
 
-	if _, err := issues_model.IssueList(issues).LoadRepositories(ctx); err != nil {
+	if _, err := issues.LoadRepositories(ctx); err != nil {
 		ctx.ServerError("LoadRepositories", err)
 		return
 	}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 5ee5ead121..6da9edfd0b 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -378,9 +378,14 @@ func UpdateIssueProject(ctx *context.Context) {
 		return
 	}
 
+	if err := issues.LoadProjects(ctx); err != nil {
+		ctx.ServerError("LoadProjects", err)
+		return
+	}
+
 	projectID := ctx.FormInt64("id")
 	for _, issue := range issues {
-		oldProjectID := issue.ProjectID()
+		oldProjectID := issue.Project.ID
 		if oldProjectID == projectID {
 			continue
 		}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index f429655541..0bc1684489 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -192,9 +192,9 @@
 			</div>
 		</div>
 		<div class="ui select-project list">
-			<span class="no-select item {{if .Issue.ProjectID}}gt-hidden{{end}}">{{.locale.Tr "repo.issues.new.no_projects"}}</span>
+			<span class="no-select item {{if .Issue.Project}}gt-hidden{{end}}">{{.locale.Tr "repo.issues.new.no_projects"}}</span>
 			<div class="selected">
-				{{if .Issue.ProjectID}}
+				{{if .Issue.Project}}
 					<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link}}">
 						{{svg .Issue.Project.IconName 18 "gt-mr-3"}}{{.Issue.Project.Title}}
 					</a>