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/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/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)), diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index e9c4eca559290..17ec7deae784e 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"` - ProjectID int64 `json:"project_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 7f32876d80574..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].ProjectID) + assert.Contains(t, data[v.ID].ProjectIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 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].ProjectID) + assert.Empty(t, data[v.ID].ProjectIDs) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 0 + return len(v.ProjectIDs) == 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 // ProjectID should not be 0 + } data = append(data, &internal.IndexerData{ ID: id, @@ -705,7 +709,8 @@ func generateDefaultIndexerData() []*internal.IndexerData { LabelIDs: labelIDs, NoLabel: len(labelIDs) == 0, MilestoneID: issueIndex % 4, - ProjectID: issueIndex % 5, + 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 9861c808dcf6d..e76c00fed29c0 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,8 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD LabelIDs: labels, NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, - ProjectID: projectID, + ProjectIDs: projectIDs, + NoProject: len(projectIDs) == 0, 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 7040c2849a2b0..715ae5afd8df0 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');