diff --git a/models/issues/issue.go b/models/issues/issue.go index eab18f4892ce9..7efca6e60a37b 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -100,14 +100,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 string `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 string `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 dad21c14776f0..4b5ccfa6d2fb5 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -230,10 +230,11 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { } func (issues IssueList) getProjectIDs() []int64 { - ids := make(container.Set[int64], len(issues)) + ids := make(container.Set[int64]) for _, issue := range issues { - ids.Add(issue.ProjectID()) + ids.AddMultiple(issue.ProjectIDs()...) } + return ids.Values() } @@ -261,8 +262,14 @@ func (issues IssueList) loadProjects(ctx context.Context) error { } for _, issue := range issues { - issue.Project = projectMaps[issue.ProjectID()] + projectIDs := issue.ProjectIDs() + for _, i := range projectIDs { + if projectMaps[i] != nil { + issue.Projects = append(issue.Projects, projectMaps[i]) + } + } } + return nil } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 04d12e055cc58..dc657bdfc60cc 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -14,31 +14,27 @@ 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 - if _, 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); err != nil { - return err - } - issue.Project = &p + Where("project_issue.issue_id = ?", issue.ID).OrderBy("title"). + Find(&issue.Projects) } 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) ProjectIDs() []int64 { + return issue.projectIDs(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) - 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 @@ -104,59 +100,103 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m } // ChangeProjectAssign changes the project associated with an issue -func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error { +func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64, action string) error { ctx, committer, err := db.TxContext(db.DefaultContext) 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 { if err := issue.LoadRepo(ctx); err != nil { return err } - // Only check if we add a new project and not remove it. - if newProjectID > 0 { - newProject, err := project_model.GetProjectByID(ctx, newProjectID) - if err != nil { - return err + oldProjectIDs := issue.projectIDs(ctx) + + if len(oldProjectIDs) > 0 { + for _, i := range oldProjectIDs { + // Only check if we add a new project and not remove it. + if newProjectID > 0 { + newProject, err := project_model.GetProjectByID(ctx, newProjectID) + if err != nil { + return err + } + if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID { + return fmt.Errorf("issue's repository is not the same as project's repository") + } + } + + if action == "attach" && newProjectID > 0 { + if err := db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + }); err != nil { + return err + } + i = 0 + } else { + if action == "clear" { + if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { + return err + } + } else { + i = newProjectID + newProjectID = 0 + if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=? AND project_issue.project_id=?", issue.ID, i).Delete(&project_model.ProjectIssue{}); err != nil { + return err + } + } + } + + if i > 0 || newProjectID > 0 { + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: i, + ProjectID: newProjectID, + }); err != nil { + return err + } + } + if action != "clear" && newProjectID == 0 || newProjectID > 0 { + break + } } - if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID { - return fmt.Errorf("issue's repository is not the same as project's repository") + } else { + if action == "attach" || action == "" { + if err := db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + }); err != nil { + return err + } } - } - - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { - return err - } - if oldProjectID > 0 || newProjectID > 0 { - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeProject, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldProjectID: oldProjectID, - ProjectID: newProjectID, - }); err != nil { - return err + if newProjectID > 0 { + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: 0, + ProjectID: newProjectID, + }); err != nil { + return err + } } } - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - }) + return nil } // MoveIssueAcrossProjectBoards move a card from one board to another diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 9fd13f09956af..5c6502a5f0b38 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -223,6 +223,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { if opts.ProjectBoardID != 0 { 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 { sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) } diff --git a/models/project/issue.go b/models/project/issue.go index 3269197d6cde3..d2b73824e1a71 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -76,7 +76,7 @@ func (p *Project) NumOpenIssues() int { } // MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column -func MoveIssuesOnProjectBoard(board *Board, sortedIssueIDs map[int64]int64) error { +func MoveIssuesOnProjectBoard(board *Board, sortedIssueIDs map[int64]int64, projectID int64) error { return db.WithTx(db.DefaultContext, func(ctx context.Context) error { sess := db.GetEngine(ctx) @@ -93,7 +93,7 @@ func MoveIssuesOnProjectBoard(board *Board, sortedIssueIDs map[int64]int64) erro } 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 44609e60b2ea0..557f4766d2491 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -242,6 +242,8 @@ func FindProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, e e.Desc("updated_unix") case "leastupdate": e.Asc("updated_unix") + case "title": + e.Asc("title") default: e.Asc("created_unix") } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index b3f6024b60606..0584e3012398a 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -430,13 +430,9 @@ func UpdateIssueProject(ctx *context.Context) { } projectID := ctx.FormInt64("id") + action := ctx.FormString("action") for _, issue := range issues { - oldProjectID := issue.ProjectID() - if oldProjectID == projectID { - continue - } - - if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID, action); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } @@ -718,7 +714,7 @@ func MoveIssues(ctx *context.Context) { } } - if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { + if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs, project.ID); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 5ab8db2e057fe..dda61c1a31071 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -520,6 +520,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { Page: -1, IsClosed: util.OptionalBoolFalse, Type: project_model.TypeRepository, + SortType: "title", }) if err != nil { ctx.ServerError("GetProjects", err) @@ -530,6 +531,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { Page: -1, IsClosed: util.OptionalBoolFalse, Type: project_model.TypeOrganization, + SortType: "title", }) if err != nil { ctx.ServerError("GetProjects", err) @@ -1176,7 +1178,7 @@ func NewIssuePost(ctx *context.Context) { ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") return } - if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID, ctx.FormString("action")); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 5ee5ead12177b..c5cad41213397 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -379,13 +379,9 @@ func UpdateIssueProject(ctx *context.Context) { } projectID := ctx.FormInt64("id") + action := ctx.FormString("action") for _, issue := range issues { - oldProjectID := issue.ProjectID() - if oldProjectID == projectID { - continue - } - - if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID, action); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } @@ -688,7 +684,7 @@ func MoveIssues(ctx *context.Context) { } } - if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { + if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs, project.ID); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index a053fec066190..44fff1290c37a 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -152,7 +152,7 @@ {{if .IsProjectsEnabled}}
- {{range .ClosedProjects}} - + {{$ProjectID := .ID}} + {{$checked := false}} + {{range $.Issue.Projects}} + {{if eq .ID $ProjectID}} + {{$checked = true}} + {{end}} + {{end}} + + {{svg "octicon-check"}} + {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{end}} {{end}} -
- {{.locale.Tr "repo.issues.new.no_projects"}} +
+ {{.locale.Tr "repo.issues.new.no_projects"}}
{{end}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 394b4a69185ac..d098c74fa6256 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -64,9 +64,9 @@ {{svg "octicon-milestone" 14 "gt-mr-2"}}{{.Milestone.Name}} {{end}} - {{if .Project}} - - {{svg .Project.IconName 14 "gt-mr-2"}}{{.Project.Title}} + {{range .Projects}} + + {{svg .IconName 14 "gt-mr-2"}}{{.Title}} {{end}} {{if .Ref}} diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index f23ff45470c10..4b561e359d56e 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -231,6 +231,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');