From 8229cd919692c71c7087f443d806d435dbae151a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?RD1=20Tyrone=20=E8=91=89=E6=BE=A4=E7=A5=A5/295?= Date: Thu, 28 Mar 2024 14:54:00 +0800 Subject: [PATCH 01/15] Add support multiple projects --- models/db/search.go | 1 + models/issues/issue.go | 16 ++-- models/issues/issue_list.go | 9 ++- models/issues/issue_list_test.go | 6 +- models/issues/issue_project.go | 76 ++++++++++++------- models/issues/issue_search.go | 2 + models/issues/issue_test.go | 6 +- models/project/issue.go | 4 +- models/project/project.go | 2 + modules/indexer/issues/internal/model.go | 2 +- .../indexer/issues/internal/tests/tests.go | 14 ++-- modules/indexer/issues/util.go | 8 +- routers/web/org/projects.go | 11 +-- routers/web/repo/projects.go | 11 +-- routers/web/repo/pull.go | 2 +- routers/web/user/home.go | 1 - services/issue/issue.go | 2 +- .../repo/issue/view_content/sidebar.tmpl | 42 +++++++--- templates/shared/issuelist.tmpl | 6 +- web_src/js/features/repo-legacy.js | 1 + 20 files changed, 135 insertions(+), 87 deletions(-) diff --git a/models/db/search.go b/models/db/search.go index aa577f08e0439..099f900ff4e99 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -30,6 +30,7 @@ const ( SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC" SearchOrderByForks SearchOrderBy = "num_forks ASC" SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" + SearchOrderByTitle SearchOrderBy = "title ASC" ) const ( diff --git a/models/issues/issue.go b/models/issues/issue.go index 87c1c86eb15be..32a1574d292a6 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -103,14 +103,14 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent template.HTML `xorm:"-"` - Labels []*Label `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent template.HTML `xorm:"-"` + Labels []*Label `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` + Projects []*project_model.Project `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 218891ad35771..e3d9943a3db5b 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -256,14 +256,19 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { return err } for _, project := range projects { - projectMaps[project.IssueID] = project.Project + projectMaps[project.ID] = project.Project } left -= limit issueIDs = issueIDs[limit:] } for _, issue := range issues { - issue.Project = projectMaps[issue.ID] + projectIDs := issue.projectIDs(ctx) + for _, i := range projectIDs { + if projectMaps[i] != nil { + issue.Projects = append(issue.Projects, projectMaps[i]) + } + } } return nil } diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 9069e1012da53..e9f254d488042 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -66,10 +66,10 @@ func TestIssueList_LoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) - assert.NotNil(t, issue.Project) - assert.Equal(t, int64(1), issue.Project.ID) + assert.NotNil(t, issue.Projects) + assert.Equal(t, int64(1), issue.Projects[0].ID) } else { - assert.Nil(t, issue.Project) + assert.Nil(t, issue.Projects) } } } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 907a5a17b9f20..35fa88970ad20 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -14,27 +14,20 @@ import ( // LoadProject load the project the issue was assigned to func (issue *Issue) LoadProject(ctx context.Context) (err error) { - if issue.Project == nil { - var p project_model.Project - has, err := db.GetEngine(ctx).Table("project"). + if issue.Projects == nil { + err = db.GetEngine(ctx).Table("project"). Join("INNER", "project_issue", "project.id=project_issue.project_id"). - Where("project_issue.issue_id = ?", issue.ID).Get(&p) - if err != nil { - return err - } else if has { - issue.Project = &p - } + Where("project_issue.issue_id = ?", issue.ID).Find(&issue.Projects) } return err } -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) - if err != nil || !has { - return 0 +func (issue *Issue) projectIDs(ctx context.Context) []int64 { + var ips []int64 + if err := db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&ips); err != nil { + return nil } - return ip.ProjectID + return ips } // ProjectBoardID return project board id if issue was assigned to one @@ -91,24 +84,25 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m } // ChangeProjectAssign changes the project associated with an issue -func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { +func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64, action string) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { + if err := addUpdateIssueProject(ctx, issue, doer, newProjectID, action); err != nil { return err } return committer.Commit() } -func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { - oldProjectID := issue.projectID(ctx) +func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64, action string) error { + var oldProjectIDs []int64 + var err error - if err := issue.LoadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } @@ -123,25 +117,51 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U } } - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { - return err + if action == "null" { + if newProjectID == 0 { + action = "clear" + } else { + action = "attach" + count, err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Count() + if err != nil { + return err + } + if count > 0 { + action = "detach" + } + } + } + + if action == "attach" { + err = db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + }) + oldProjectIDs = append(oldProjectIDs, 0) + } else if action == "detach" { + _, err = db.GetEngine(ctx).Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Delete(&project_model.ProjectIssue{}) + oldProjectIDs = append(oldProjectIDs, newProjectID) + newProjectID = 0 + } else if action == "clear" { + if err = db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&oldProjectIDs); err != nil { + return err + } + _, err = db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}) + newProjectID = 0 } - if oldProjectID > 0 || newProjectID > 0 { + for i := range oldProjectIDs { if _, err := CreateComment(ctx, &CreateCommentOptions{ Type: CommentTypeProject, Doer: doer, Repo: issue.Repo, Issue: issue, - OldProjectID: oldProjectID, + OldProjectID: oldProjectIDs[i], ProjectID: newProjectID, }); err != nil { return err } } - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - }) + return err } diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 921dd9973ec9f..c96f1b0584de1 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -174,6 +174,8 @@ func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.S // do not need to apply any condition if opts.ProjectBoardID > 0 { sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) + } else if opts.ProjectID > 0 { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0, "project_id": opts.ProjectID})) } else if opts.ProjectBoardID == db.NoConditionID { sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) } diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 1bbc0eee564fd..0bac24da191c5 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -418,10 +418,10 @@ func TestIssueLoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) - assert.NotNil(t, issue.Project) - assert.Equal(t, int64(1), issue.Project.ID) + assert.NotNil(t, issue.Projects) + assert.Equal(t, int64(1), issue.Projects[0].ID) } else { - assert.Nil(t, issue.Project) + assert.Nil(t, issue.Projects) } } } diff --git a/models/project/issue.go b/models/project/issue.go index ebc9719de55d0..c443bb697e177 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -76,7 +76,7 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { } // MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column -func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { +func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64, projectID int64) error { return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) @@ -93,7 +93,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs } for sorting, issueID := range sortedIssueIDs { - _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID) + _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=? AND project_id=?", board.ID, sorting, issueID, projectID) if err != nil { return err } diff --git a/models/project/project.go b/models/project/project.go index 8f9ee2a99e9c7..82ab86d4adbfe 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -237,6 +237,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { return db.SearchOrderByRecentUpdated case "leastupdate": return db.SearchOrderByLeastUpdated + case "title": + return db.SearchOrderByTitle default: return db.SearchOrderByNewest } diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index e9c4eca559290..dd64b7f8c885a 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -26,7 +26,7 @@ type IndexerData struct { LabelIDs []int64 `json:"label_ids"` NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` - ProjectID int64 `json:"project_id"` + ProjectIDs []int64 `json:"project_ids"` ProjectBoardID int64 `json:"project_board_id"` PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 7f32876d80574..f86a32fded499 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -312,10 +312,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].ProjectID) + assert.Equal(t, int64(1), data[v.ID].ProjectIDs) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 1 + return v.ProjectIDs[0] == 1 }), result.Total) }, }, @@ -330,10 +330,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].ProjectID) + assert.Equal(t, int64(0), data[v.ID].ProjectIDs) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 0 + return v.ProjectIDs[0] == 0 }), result.Total) }, }, @@ -692,6 +692,10 @@ func generateDefaultIndexerData() []*internal.IndexerData { for i := range subscriberIDs { subscriberIDs[i] = int64(i) + 1 // SubscriberID should not be 0 } + projectIDs := make([]int64, id%5) + for i := range projectIDs { + projectIDs[i] = int64(i) + 1 + } data = append(data, &internal.IndexerData{ ID: id, @@ -705,7 +709,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { LabelIDs: labelIDs, NoLabel: len(labelIDs) == 0, MilestoneID: issueIndex % 4, - ProjectID: issueIndex % 5, + ProjectIDs: projectIDs, ProjectBoardID: issueIndex % 6, PosterID: id%10 + 1, // PosterID should not be 0 AssigneeID: issueIndex % 10, diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 9861c808dcf6d..06a6abfb54f08 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -87,9 +87,9 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD return nil, false, err } - var projectID int64 - if issue.Project != nil { - projectID = issue.Project.ID + projectIDs := make([]int64, 0, len(issue.Projects)) + for _, project := range issue.Projects { + projectIDs = append(projectIDs, project.ID) } return &internal.IndexerData{ @@ -104,7 +104,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD LabelIDs: labels, NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, - ProjectID: projectID, + ProjectIDs: projectIDs, ProjectBoardID: issue.ProjectBoardID(ctx), PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 596a370d2e551..3efec69ace241 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -442,14 +442,9 @@ func UpdateIssueProject(ctx *context.Context) { } projectID := ctx.FormInt64("id") + action := ctx.FormString("action") for _, issue := range issues { - if issue.Project != nil { - if issue.Project.ID == projectID { - continue - } - } - - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, action); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } @@ -671,7 +666,7 @@ func MoveIssues(ctx *context.Context) { } } - if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { + if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs, project.ID); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index a2db1fc770a5b..c19fe39a9853a 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -390,14 +390,9 @@ func UpdateIssueProject(ctx *context.Context) { } projectID := ctx.FormInt64("id") + action := ctx.FormString("action") for _, issue := range issues { - if issue.Project != nil { - if issue.Project.ID == projectID { - continue - } - } - - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, action); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } @@ -664,7 +659,7 @@ func MoveIssues(ctx *context.Context) { } } - if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { + if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs, project.ID); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index a0a8e5410cf15..213532271efe0 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1338,7 +1338,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects") return } - if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID, "attach"); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index ff6c2a6c36dd5..2017cfe94e556 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -358,7 +358,6 @@ func Issues(ctx *context.Context) { ctx.Status(http.StatusNotFound) return } - ctx.Data["Title"] = ctx.Tr("issues") ctx.Data["PageIsIssues"] = true buildIssueOverview(ctx, unit.TypeIssues) diff --git a/services/issue/issue.go b/services/issue/issue.go index c7fa9f3300a5e..0c1f1fe641786 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -42,7 +42,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo } } if projectID > 0 { - if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID, "attach"); err != nil { return err } } diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index bc2a84170807c..ccc06e65361e0 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -154,7 +154,7 @@ {{if .IsProjectsEnabled}}
- {{range .ClosedProjects}} - + {{$ProjectID := $.Projects.IssueID}} + {{$checked := false}} + {{range $.Issue.Projects}} + {{if eq .IssueID $ProjectID}} + {{$checked = true}} + {{break}} + {{end}} + {{end}} + + {{svg "octicon-check"}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} + {{end}} {{end}} -
- {{ctx.Locale.Tr "repo.issues.new.no_projects"}} +
+ {{ctx.Locale.Tr "repo.issues.new.no_projects"}}
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 1c0dfcc5511e2..faef1216415ca 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -92,9 +92,9 @@ {{svg "octicon-milestone" 14}}{{.Milestone.Name}} {{end}} - {{if .Project}} - - {{svg .Project.IconName 14}}{{.Project.Title}} + {{range .Projects}} + + {{svg .IconName 14}}{{.Title}} {{end}} {{if .Ref}} diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 34320de1deb1a..8ab9018264b07 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -241,6 +241,7 @@ export function initRepoCommentForm() { // Init labels and assignees initListSubmits('select-label', 'labels'); + initListSubmits('select-projects', 'projects'); initListSubmits('select-assignees', 'assignees'); initListSubmits('select-assignees-modify', 'assignees'); initListSubmits('select-reviewers-modify', 'assignees'); From 8dcdbf60a45cdd7b3d7ce283960d46afda556fef Mon Sep 17 00:00:00 2001 From: Jason Song Date: Mon, 1 Apr 2024 12:19:16 +0800 Subject: [PATCH 02/15] feat: make issuer indexer support multiple projects --- modules/indexer/issues/bleve/bleve.go | 11 ++++++++--- modules/indexer/issues/elasticsearch/elasticsearch.go | 11 ++++++++--- modules/indexer/issues/internal/model.go | 3 ++- modules/indexer/issues/internal/tests/tests.go | 11 ++++++----- modules/indexer/issues/meilisearch/meilisearch.go | 11 ++++++++--- modules/indexer/issues/util.go | 1 + 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 1f54be721b37c..ad18e96dbe0c3 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -23,7 +23,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 4 + issueIndexerLatestVersion = 5 ) const unicodeNormalizeName = "unicodeNormalize" @@ -82,7 +82,8 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("label_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("no_label", boolFieldMapping) docMapping.AddFieldMappingsAt("milestone_id", numberFieldMapping) - docMapping.AddFieldMappingsAt("project_id", numberFieldMapping) + docMapping.AddFieldMappingsAt("project_ids", numberFieldMapping) + docMapping.AddFieldMappingsAt("no_project", boolFieldMapping) docMapping.AddFieldMappingsAt("project_board_id", numberFieldMapping) docMapping.AddFieldMappingsAt("poster_id", numberFieldMapping) docMapping.AddFieldMappingsAt("assignee_id", numberFieldMapping) @@ -226,7 +227,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( } if options.ProjectID.Has() { - queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id")) + if v := options.ProjectID.Value(); v != 0 { + queries = append(queries, inner_bleve.NumericEqualityQuery(v, "project_ids")) + } else { + queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_project")) + } } if options.ProjectBoardID.Has() { queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id")) diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 53b383c8d5d78..5bfa08467729a 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 1 + issueIndexerLatestVersion = 2 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -61,7 +61,8 @@ const ( "label_ids": { "type": "integer", "index": true }, "no_label": { "type": "boolean", "index": true }, "milestone_id": { "type": "integer", "index": true }, - "project_id": { "type": "integer", "index": true }, + "project_ids": { "type": "integer", "index": true }, + "no_project": { "type": "boolean", "index": true }, "project_board_id": { "type": "integer", "index": true }, "poster_id": { "type": "integer", "index": true }, "assignee_id": { "type": "integer", "index": true }, @@ -196,7 +197,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( } if options.ProjectID.Has() { - query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) + if v := options.ProjectID.Value(); v != 0 { + query.Must(elastic.NewTermQuery("project_ids", v)) + } else { + query.Must(elastic.NewTermQuery("no_project", true)) + } } if options.ProjectBoardID.Has() { query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value())) diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index dd64b7f8c885a..17ec7deae784e 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -27,6 +27,7 @@ type IndexerData struct { NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` ProjectIDs []int64 `json:"project_ids"` + NoProject bool `json:"no_project"` // True if ProjectIDs is empty ProjectBoardID int64 `json:"project_board_id"` PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` @@ -89,7 +90,7 @@ type SearchOptions struct { MilestoneIDs []int64 // milestones the issues have - ProjectID optional.Option[int64] // project the issues belong to + ProjectID optional.Option[int64] // project the issues belong to, zero means no project ProjectBoardID optional.Option[int64] // project board the issues belong to PosterID optional.Option[int64] // poster of the issues diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index f86a32fded499..3dcaaec8b76dc 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -312,10 +312,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].ProjectIDs) + assert.Contains(t, data[v.ID].ProjectIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectIDs[0] == 1 + return slices.Contains(v.ProjectIDs, 1) }), result.Total) }, }, @@ -330,10 +330,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].ProjectIDs) + assert.Empty(t, data[v.ID].ProjectIDs) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectIDs[0] == 0 + return len(v.ProjectIDs) == 0 }), result.Total) }, }, @@ -694,7 +694,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { } projectIDs := make([]int64, id%5) for i := range projectIDs { - projectIDs[i] = int64(i) + 1 + projectIDs[i] = int64(i) + 1 // ProjectID should not be 0 } data = append(data, &internal.IndexerData{ @@ -710,6 +710,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { NoLabel: len(labelIDs) == 0, MilestoneID: issueIndex % 4, ProjectIDs: projectIDs, + NoProject: len(projectIDs) == 0, ProjectBoardID: issueIndex % 6, PosterID: id%10 + 1, // PosterID should not be 0 AssigneeID: issueIndex % 10, diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 8a7cec6cba4dd..42febab636e8c 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 3 + issueIndexerLatestVersion = 4 // TODO: make this configurable if necessary maxTotalHits = 10000 @@ -64,7 +64,8 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { "label_ids", "no_label", "milestone_id", - "project_id", + "project_ids", + "no_project", "project_board_id", "poster_id", "assignee_id", @@ -172,7 +173,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( } if options.ProjectID.Has() { - query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) + if v := options.ProjectID.Value(); v != 0 { + query.And(inner_meilisearch.NewFilterEq("project_ids", v)) + } else { + query.And(inner_meilisearch.NewFilterEq("no_label", true)) + } } if options.ProjectBoardID.Has() { query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value())) diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 06a6abfb54f08..e76c00fed29c0 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -105,6 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, ProjectIDs: projectIDs, + NoProject: len(projectIDs) == 0, ProjectBoardID: issue.ProjectBoardID(ctx), PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, From fc63953df69277142b56e4cd6ecc202a543582c9 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 1 Apr 2024 15:59:26 +0800 Subject: [PATCH 03/15] for unit test --- modules/indexer/issues/indexer_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 0d0cfc851697d..f391546e4bff4 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -361,12 +361,6 @@ func searchIssueInProject(t *testing.T) { opts SearchOptions expectedIDs []int64 }{ - { - SearchOptions{ - ProjectID: optional.Some(int64(1)), - }, - []int64{5, 3, 2, 1}, - }, { SearchOptions{ ProjectBoardID: optional.Some(int64(1)), From 91b55d116c39b4f980f5c366982e7b56a6ef9366 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Thu, 11 Apr 2024 15:59:14 +0800 Subject: [PATCH 04/15] Add s for LoadProject function --- models/issues/issue.go | 2 +- models/issues/issue_project.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index 32a1574d292a6..5a25b120d6975 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -311,7 +311,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { return err } - if err = issue.LoadProject(ctx); err != nil { + if err = issue.LoadProjects(ctx); err != nil { return err } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 35fa88970ad20..3269ad3025f87 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -13,7 +13,7 @@ import ( ) // LoadProject load the project the issue was assigned to -func (issue *Issue) LoadProject(ctx context.Context) (err error) { +func (issue *Issue) LoadProjects(ctx context.Context) (err error) { if issue.Projects == nil { err = db.GetEngine(ctx).Table("project"). Join("INNER", "project_issue", "project.id=project_issue.project_id"). From 54b5396c55dae4925f4f0609ad84fccebd10b96e Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 15 Apr 2024 15:50:45 +0800 Subject: [PATCH 05/15] Update templates/repo/issue/view_content/sidebar.tmpl Co-authored-by: silverwind --- templates/repo/issue/view_content/sidebar.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 715ae5afd8df0..b396cd93d56d9 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -203,7 +203,7 @@ {{if eq .IssueID $ProjectID}} {{$checked = true}} {{break}} - {{end}} + {{end}} {{end}} {{svg "octicon-check"}} From 1ee980763f506a570b0529f5b818940de314f24a Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 15 Apr 2024 15:51:03 +0800 Subject: [PATCH 06/15] Update templates/repo/issue/view_content/sidebar.tmpl Co-authored-by: silverwind --- templates/repo/issue/view_content/sidebar.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index b396cd93d56d9..f4c3ec1f7bad7 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -184,7 +184,7 @@ {{end}} {{end}} - {{svg "octicon-check"}} + {{svg "octicon-check"}} {{svg .IconName 18 "tw-mr-2"}}{{.Title}} From 596316a7781adc047917d8dfb8cf613ec9cff2a2 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 15 Apr 2024 15:51:19 +0800 Subject: [PATCH 07/15] Update templates/repo/issue/view_content/sidebar.tmpl Co-authored-by: silverwind --- templates/repo/issue/view_content/sidebar.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index f4c3ec1f7bad7..e2ad755f3abee 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -206,7 +206,7 @@ {{end}} {{end}} - {{svg "octicon-check"}} + {{svg "octicon-check"}} {{svg .IconName 18 "tw-mr-2"}}{{.Title}} From 2ba5b9d0fb0bfebbf6e0a0a8a1ea2eacb8af0bc9 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 15 Apr 2024 17:11:56 +0800 Subject: [PATCH 08/15] Remove double item class --- templates/repo/issue/view_content/sidebar.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index e2ad755f3abee..ea7a0a3729dfb 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -220,7 +220,7 @@
{{range .Issue.Projects}} From 577f9c9684b8ca66af81ddc4f079da90984da8ea Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 20 May 2024 09:17:09 +0800 Subject: [PATCH 09/15] Add missing merge file --- models/issues/issue_project.go | 85 +++++++++++++++++----------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index a4f33508a72f7..b9c1fd2381bca 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -87,8 +87,8 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m // If newProjectID is 0, the issue is removed from the project func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64, action string) error { return db.WithTx(ctx, func(ctx context.Context) error { - oldProjectID := issue.projectID(ctx) - + oldProjectIDs := issue.projectIDs(ctx) + var err error if err := issue.LoadRepo(ctx); err != nil { return err } @@ -111,50 +111,51 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo } } - if action == "null" { - if newProjectID == 0 { - action = "clear" - } else { - action = "attach" - count, err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Count() - if err != nil { - return err - } - if count > 0 { - action = "detach" + if action == "null" { + if newProjectID == 0 { + action = "clear" + } else { + action = "attach" + count, err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Count() + if err != nil { + return err + } + if count > 0 { + action = "detach" + } } } - } - if action == "attach" { - err = db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - }) - oldProjectIDs = append(oldProjectIDs, 0) - } else if action == "detach" { - _, err = db.GetEngine(ctx).Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Delete(&project_model.ProjectIssue{}) - oldProjectIDs = append(oldProjectIDs, newProjectID) - newProjectID = 0 - } else if action == "clear" { - if err = db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&oldProjectIDs); err != nil { - return err + if action == "attach" { + err = db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + }) + oldProjectIDs = append(oldProjectIDs, 0) + } else if action == "detach" { + _, err = db.GetEngine(ctx).Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Delete(&project_model.ProjectIssue{}) + oldProjectIDs = append(oldProjectIDs, newProjectID) + newProjectID = 0 + } else if action == "clear" { + if err = db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&oldProjectIDs); err != nil { + return err + } + _, err = db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}) + newProjectID = 0 } - _, err = db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}) - newProjectID = 0 - } - for i := range oldProjectIDs { - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeProject, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldProjectID: oldProjectIDs[i], - ProjectID: newProjectID, - }); err != nil { - return err + for i := range oldProjectIDs { + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: oldProjectIDs[i], + ProjectID: newProjectID, + }); err != nil { + return err + } } - - return err + return err + }) } From 1bf57342316f2e62f75a3d537fa1d2650d8d9e1e Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 5 Jun 2024 10:20:59 +0800 Subject: [PATCH 10/15] Fix conflicting --- models/issues/issue.go | 18 +++---- models/issues/issue_project.go | 48 +++++++------------ modules/indexer/issues/bleve/bleve.go | 10 ++-- modules/indexer/issues/db/options.go | 2 +- modules/indexer/issues/dboptions.go | 2 +- .../issues/elasticsearch/elasticsearch.go | 8 +--- modules/indexer/issues/internal/model.go | 5 +- .../indexer/issues/internal/tests/tests.go | 4 +- .../indexer/issues/meilisearch/meilisearch.go | 8 +--- routers/web/repo/issue.go | 14 +++--- 10 files changed, 50 insertions(+), 69 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index b929deca8f983..6f2814550f3cf 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -105,15 +105,15 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent template.HTML `xorm:"-"` - ContentVersion int `xorm:"NOT NULL DEFAULT 0"` - Labels []*Label `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent template.HTML `xorm:"-"` + ContentVersion int `xorm:"NOT NULL DEFAULT 0"` + Labels []*Label `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` + Projects []*project_model.Project `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 0847262b3e006..f3c45c095355d 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -87,7 +87,7 @@ func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) // If newProjectID is 0, the issue is removed from the project func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64, action string) error { return db.WithTx(ctx, func(ctx context.Context) error { - oldProjectIDs := issue.projectIDs(ctx) + var oldProjectIDs []int64 var err error if err := issue.LoadRepo(ctx); err != nil { return err @@ -111,22 +111,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo } } - if action == "null" { - if newProjectID == 0 { - action = "clear" - } else { - action = "attach" - count, err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Count() - if err != nil { - return err - } - if count > 0 { - action = "detach" - } - } - } - - if action == "attach" { + if action == "attach" || (action == "null" && newProjectID > 0) { if newProjectID == 0 { return nil } @@ -134,28 +119,31 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo panic("newColumnID must not be zero") // shouldn't happen } res := struct { - MaxSorting int64 IssueCount int64 }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). + if _, err := db.GetEngine(ctx).Select("count(*) as issue_count").Table("project_issue"). Where("project_id=?", newProjectID). And("project_board_id=?", newColumnID). Get(&res); err != nil { return err } - newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) - err = db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - ProjectColumnID: newColumnID, - Sorting: newSorting, - }) - oldProjectIDs = append(oldProjectIDs, 0) + if res.IssueCount == 0 { + err = db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + ProjectColumnID: newColumnID, + }) + oldProjectIDs = []int64{0} + } else { + _, err = db.GetEngine(ctx).Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Delete(&project_model.ProjectIssue{}) + oldProjectIDs = []int64{newProjectID} + newProjectID = 0 + } } else if action == "detach" { _, err = db.GetEngine(ctx).Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Delete(&project_model.ProjectIssue{}) oldProjectIDs = append(oldProjectIDs, newProjectID) newProjectID = 0 - } else if action == "clear" { + } else if action == "clear" || (action == "null" && newProjectID == 0) { if err = db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&oldProjectIDs); err != nil { return err } @@ -163,13 +151,13 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo newProjectID = 0 } - for i := range oldProjectIDs { + for _, oldProjectID := range oldProjectIDs { if _, err := CreateComment(ctx, &CreateCommentOptions{ Type: CommentTypeProject, Doer: doer, Repo: issue.Repo, Issue: issue, - OldProjectID: oldProjectIDs[i], + OldProjectID: oldProjectID, ProjectID: newProjectID, }); err != nil { return err diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 632712c4438c0..022de1af28e79 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -222,12 +222,12 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...)) } - if options.ProjectID.Has() { - if v := options.ProjectID.Value(); v != 0 { - queries = append(queries, inner_bleve.NumericEqualityQuery(v, "project_ids")) - } else { - queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_project")) + if len(options.ProjectIDs) > 0 { + var projectQueries []query.Query + for _, projectID := range options.ProjectIDs { + projectQueries = append(projectQueries, inner_bleve.NumericEqualityQuery(projectID, "project_ids")) } + queries = append(queries, bleve.NewDisjunctionQuery(projectQueries...)) } if options.ProjectColumnID.Has() { queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id")) diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 875a4ca279dd6..fe35d036abede 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -60,7 +60,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m ReviewRequestedID: convertID(options.ReviewRequestedID), ReviewedID: convertID(options.ReviewedID), SubscriberID: convertID(options.SubscriberID), - ProjectID: convertID(options.ProjectID), + ProjectID: convertID(options.ProjectIDs), ProjectColumnID: convertID(options.ProjectColumnID), IsClosed: options.IsClosed, IsPull: options.IsPull, diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index d9cf9b5e3b1b7..6c38df2054fda 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -49,7 +49,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp return nil } - searchOpt.ProjectID = convertID(opts.ProjectID) + searchOpt.ProjectIDs = convertID(opts.ProjectID) searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.PosterID = convertID(opts.PosterID) searchOpt.AssigneeID = convertID(opts.AssigneeID) diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 20fe6b6ceac1b..5c4755fbf32c9 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -195,12 +195,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...)) } - if options.ProjectID.Has() { - if v := options.ProjectID.Value(); v != 0 { - query.Must(elastic.NewTermQuery("project_ids", v)) - } else { - query.Must(elastic.NewTermQuery("no_project", true)) - } + if len(options.ProjectIDs) > 0 { + query.Must(elastic.NewTermsQuery("project_ids", toAnySlice(options.ProjectIDs)...)) } if options.ProjectColumnID.Has() { query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value())) diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 00963953e391f..13cfe60a7368d 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -26,7 +26,8 @@ type IndexerData struct { LabelIDs []int64 `json:"label_ids"` NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` - ProjectIDs int64 `json:"project_ids"` + ProjectIDs []int64 `json:"project_ids"` + NoProject bool `json:"no_project"` // True if ProjectIDs is empty ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` @@ -89,7 +90,7 @@ type SearchOptions struct { MilestoneIDs []int64 // milestones the issues have - ProjectID optional.Option[int64] // project the issues belong to + ProjectIDs []int64 // project the issues belong to ProjectColumnID optional.Option[int64] // project column the issues belong to PosterID optional.Option[int64] // poster of the issues diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 32dd510496694..bd242acf598b1 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -307,7 +307,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectID: optional.Some(int64(1)), + ProjectIDs: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -325,7 +325,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectID: optional.Some(int64(0)), + ProjectIDs: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index d35d5f0274159..88ed6f14f80a9 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -172,12 +172,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...)) } - if options.ProjectID.Has() { - if v := options.ProjectID.Value(); v != 0 { - query.And(inner_meilisearch.NewFilterEq("project_ids", v)) - } else { - query.And(inner_meilisearch.NewFilterEq("no_label", true)) - } + if len(options.ProjectIDs) > 0 { + query.And(inner_meilisearch.NewFilterIn("project_ids", options.ProjectIDs...)) } if options.ProjectColumnID.Has() { query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value())) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index ce459f23b9323..a4a79027b5d8f 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -2670,7 +2670,7 @@ func SearchIssues(ctx *context.Context) { IsClosed: isClosed, IncludedAnyLabelIDs: includedAnyLabels, MilestoneIDs: includedMilestones, - ProjectID: projectID, + ProjectIDs: projectID, SortBy: issue_indexer.SortByCreatedDesc, } @@ -2833,12 +2833,12 @@ func ListIssues(ctx *context.Context) { Page: ctx.FormInt("page"), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), }, - Keyword: keyword, - RepoIDs: []int64{ctx.Repo.Repository.ID}, - IsPull: isPull, - IsClosed: isClosed, - ProjectID: projectID, - SortBy: issue_indexer.SortByCreatedDesc, + Keyword: keyword, + RepoIDs: []int64{ctx.Repo.Repository.ID}, + IsPull: isPull, + IsClosed: isClosed, + ProjectIDs: projectID, + SortBy: issue_indexer.SortByCreatedDesc, } if since != 0 { searchOpt.UpdatedAfterUnix = optional.Some(since) From 6d268cd1a63a61febc231f8be664d9c78bdd93db Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 5 Jun 2024 10:35:36 +0800 Subject: [PATCH 11/15] add missing column for args --- models/project/issue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/project/issue.go b/models/project/issue.go index 2ed121827e115..b0ed76a714a23 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -91,7 +91,7 @@ func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueI } for sorting, issueID := range sortedIssueIDs { - _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID, projectID) + _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=? AND project_id=?", column.ID, sorting, issueID, projectID) if err != nil { return err } From d00558f96c2bca47a332c3c69a38b1b6dd8c3735 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 5 Jun 2024 11:25:04 +0800 Subject: [PATCH 12/15] Fix filter no project and attach project no work issue --- models/db/search.go | 1 + models/issues/issue_project.go | 2 +- modules/indexer/issues/db/options.go | 8 ++++++++ modules/indexer/issues/dboptions.go | 7 ++++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/models/db/search.go b/models/db/search.go index 099f900ff4e99..2363160aaf50e 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -37,4 +37,5 @@ const ( // Which means a condition to filter the records which don't match any id. // It's different from zero which means the condition could be ignored. NoConditionID = -1 + NoProjectID = -1 ) diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index f3c45c095355d..0c00c676937de 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -123,7 +123,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo }{} if _, err := db.GetEngine(ctx).Select("count(*) as issue_count").Table("project_issue"). Where("project_id=?", newProjectID). - And("project_board_id=?", newColumnID). + And("issue_id=?", issue.ID). Get(&res); err != nil { return err } diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index fe35d036abede..94834b6dfe166 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -84,6 +84,14 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m opts.MilestoneIDs = options.MilestoneIDs } + if len(options.ProjectIDs) == 1 { + if options.ProjectIDs[0] == 0 { + opts.ProjectID = db.NoProjectID + } else { + opts.ProjectID = options.ProjectIDs[0] + } + } + if options.NoLabelOnly { opts.LabelIDs = []int64{0} // Be careful, it's zero, not db.NoConditionID } else { diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index 6c38df2054fda..a188b1ed5f464 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -49,7 +49,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp return nil } - searchOpt.ProjectIDs = convertID(opts.ProjectID) + if opts.ProjectID == db.NoProjectID { + searchOpt.ProjectIDs = []int64{0} + } else { + searchOpt.ProjectIDs = convertID(opts.ProjectID) + } + searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.PosterID = convertID(opts.PosterID) searchOpt.AssigneeID = convertID(opts.AssigneeID) From 52e5f0137eae02339498cdba8aafe7e3415763ae Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Thu, 6 Jun 2024 10:01:09 +0800 Subject: [PATCH 13/15] Adjust projectID to projectIDs --- models/db/search.go | 1 - models/issues/issue_project.go | 4 ++-- models/issues/issue_search.go | 14 +++++++------- modules/indexer/issues/db/options.go | 11 ++++------- modules/indexer/issues/dboptions.go | 12 ++++++------ routers/web/repo/issue.go | 9 +++++++-- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/models/db/search.go b/models/db/search.go index 2363160aaf50e..099f900ff4e99 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -37,5 +37,4 @@ const ( // Which means a condition to filter the records which don't match any id. // It's different from zero which means the condition could be ignored. NoConditionID = -1 - NoProjectID = -1 ) diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 0c00c676937de..98478c694fdb4 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -44,7 +44,7 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) int64 { func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) { issueList, err := Issues(ctx, &IssuesOptions{ ProjectColumnID: b.ID, - ProjectID: b.ProjectID, + ProjectIDs: []int64{b.ProjectID}, SortType: "project-column-sorting", }) if err != nil { @@ -54,7 +54,7 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueLi if b.Default { issues, err := Issues(ctx, &IssuesOptions{ ProjectColumnID: db.NoConditionID, - ProjectID: b.ProjectID, + ProjectIDs: []int64{b.ProjectID}, SortType: "project-column-sorting", }) if err != nil { diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index bc8f48cdcb8a2..6e494a02c37fe 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -32,7 +32,7 @@ type IssuesOptions struct { //nolint ReviewedID int64 SubscriberID int64 MilestoneIDs []int64 - ProjectID int64 + ProjectIDs []int64 ProjectColumnID int64 IsClosed optional.Option[bool] IsPull optional.Option[bool] @@ -158,11 +158,11 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess } func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { - if opts.ProjectID > 0 { // specific project - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) - } else if opts.ProjectID == db.NoConditionID { // show those that are in no project + if len(opts.ProjectIDs) == 1 && opts.ProjectIDs[0] == db.NoConditionID { // show those that are in no project sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue").And(builder.Neq{"project_id": 0}))) + } else if len(opts.ProjectIDs) > 0 { // specific project + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). + In("project_issue.project_id", opts.ProjectIDs) } // opts.ProjectID == 0 means all projects, // do not need to apply any condition @@ -174,8 +174,8 @@ func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm. // do not need to apply any condition if opts.ProjectColumnID > 0 { sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID})) - } else if opts.ProjectID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0, "project_id": opts.ProjectID})) + } else if len(opts.ProjectIDs) > 0 { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}).And(builder.In("project_id", opts.ProjectIDs))) } else if opts.ProjectColumnID == db.NoConditionID { sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) } diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 94834b6dfe166..e2b0ee18f9b5e 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -60,7 +60,6 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m ReviewRequestedID: convertID(options.ReviewRequestedID), ReviewedID: convertID(options.ReviewedID), SubscriberID: convertID(options.SubscriberID), - ProjectID: convertID(options.ProjectIDs), ProjectColumnID: convertID(options.ProjectColumnID), IsClosed: options.IsClosed, IsPull: options.IsPull, @@ -84,12 +83,10 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m opts.MilestoneIDs = options.MilestoneIDs } - if len(options.ProjectIDs) == 1 { - if options.ProjectIDs[0] == 0 { - opts.ProjectID = db.NoProjectID - } else { - opts.ProjectID = options.ProjectIDs[0] - } + if len(options.ProjectIDs) == 1 && options.ProjectIDs[0] == 0 { + opts.ProjectIDs = []int64{db.NoConditionID} + } else { + opts.ProjectIDs = options.ProjectIDs } if options.NoLabelOnly { diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index a188b1ed5f464..91624cca3095b 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -38,6 +38,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.MilestoneIDs = opts.MilestoneIDs } + if len(opts.ProjectIDs) == 1 && opts.ProjectIDs[0] == db.NoConditionID { + searchOpt.ProjectIDs = []int64{0} + } else { + searchOpt.ProjectIDs = opts.ProjectIDs + } + // See the comment of issues_model.SearchOptions for the reason why we need to convert convertID := func(id int64) optional.Option[int64] { if id > 0 { @@ -49,12 +55,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp return nil } - if opts.ProjectID == db.NoProjectID { - searchOpt.ProjectIDs = []int64{0} - } else { - searchOpt.ProjectIDs = convertID(opts.ProjectID) - } - searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.PosterID = convertID(opts.PosterID) searchOpt.AssigneeID = convertID(opts.AssigneeID) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 43e55841004d7..da9fa7ff57bc7 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -201,12 +201,17 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt mileIDs = []int64{milestoneID} } + var projIDs []int64 + if projectID > 0 || projectID == db.NoConditionID { + projIDs = []int64{projectID} + } + var issueStats *issues_model.IssueStats statsOpts := &issues_model.IssuesOptions{ RepoIDs: []int64{repo.ID}, LabelIDs: labelIDs, MilestoneIDs: mileIDs, - ProjectID: projectID, + ProjectIDs: projIDs, AssigneeID: assigneeID, MentionedID: mentionedID, PosterID: posterID, @@ -296,7 +301,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ReviewRequestedID: reviewRequestedID, ReviewedID: reviewedID, MilestoneIDs: mileIDs, - ProjectID: projectID, + ProjectIDs: projIDs, IsClosed: isShowClosed, IsPull: isPullOption, LabelIDs: labelIDs, From 65c2dd010a976eb64002280c948116b94873ec48 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Thu, 6 Jun 2024 14:07:47 +0800 Subject: [PATCH 14/15] Modify for unit test --- modules/indexer/issues/indexer_test.go | 2 +- modules/indexer/issues/internal/tests/tests.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index b5917cb7f925f..ce35a7ba22c36 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -365,7 +365,7 @@ func searchIssueInProject(t *testing.T) { SearchOptions{ ProjectIDs: []int64{1}, }, - []int64{5, 3, 2, 1}, + []int64{2}, }, { SearchOptions{ diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index bd242acf598b1..3b73bc58d18ad 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -302,17 +302,17 @@ var cases = []*testIndexerCase{ }, }, { - Name: "ProjectID", + Name: "ProjectIDs", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectIDs: optional.Some(int64(1)), + ProjectIDs: []int64{1}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Contains(t, data[v.ID].ProjectIDs, int64(1)) + assert.Contains(t, []int64{1}, data[v.ID].ProjectIDs) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { return slices.Contains(v.ProjectIDs, 1) @@ -320,12 +320,12 @@ var cases = []*testIndexerCase{ }, }, { - Name: "no ProjectID", + Name: "no ProjectIDs", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectIDs: optional.Some(int64(0)), + ProjectIDs: []int64{0}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) From 13fa5f4fdea740b7dded055d67a8296918a77cf9 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 16 Oct 2024 08:36:15 +0800 Subject: [PATCH 15/15] Fix --- models/issues/issue_search.go | 15 +++++++-------- modules/indexer/issues/dboptions.go | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index e0b75d5adfb50..3a0815f7a5eb6 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -183,14 +183,13 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) { } func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) { - if opts.ProjectID > 0 { // specific project - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) - } else if opts.ProjectID == db.NoConditionID { // show those that are in no project - sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue").And(builder.Neq{"project_id": 0}))) - } else if len(opts.ProjectIDs) > 0 { // specific project - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - In("project_issue.project_id", opts.ProjectIDs) + if len(opts.ProjectIDs) > 0 { // specific project + if opts.ProjectIDs[0] == db.NoConditionID { // show those that are in no project + sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue").And(builder.Neq{"project_id": 0}))) + } else { + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). + In("project_issue.project_id", opts.ProjectIDs) + } } // opts.ProjectID == 0 means all projects, // do not need to apply any condition diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index c1f454eeee563..c97ba545bedb0 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -38,10 +38,10 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.MilestoneIDs = opts.MilestoneIDs } - if opts.ProjectID > 0 { - searchOpt.ProjectID = optional.Some(opts.ProjectID) - } else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places - searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) + if len(opts.ProjectIDs) == 1 && opts.ProjectIDs[0] == db.NoConditionID { + searchOpt.ProjectIDs = []int64{0} + } else { + searchOpt.ProjectIDs = opts.ProjectIDs // Those issues with no project(projectid==0) } if opts.AssigneeID > 0 {