diff --git a/models/issues/issue.go b/models/issues/issue.go index b0ff0adddda18..cc4b89f0f5544 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -102,14 +102,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 a932ac2554369..08e3543f89635 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -231,9 +231,13 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { func (issues IssueList) LoadProjects(ctx context.Context) error { issueIDs := issues.getIssueIDs() - projectMaps := make(map[int64]*project_model.Project, len(issues)) + issueMap := make(map[int64]*Issue, len(issues)) left := len(issueIDs) + for _, issue := range issues { + issueMap[issue.ID] = issue; + } + type projectWithIssueID struct { *project_model.Project `xorm:"extends"` IssueID int64 @@ -255,16 +259,14 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { if err != nil { return err } - for _, project := range projects { - projectMaps[project.IssueID] = project.Project - } left -= limit issueIDs = issueIDs[limit:] + for _, projectIssue := range projects { + issue := issueMap[projectIssue.IssueID] + issue.Projects = append(issue.Projects, projectIssue.Project) + } } - for _, issue := range issues { - issue.Project = projectMaps[issue.ID] - } return nil } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index cc7ffb356a6ca..3cc082292b15e 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -14,27 +14,24 @@ 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).OrderBy("title"). + 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 @@ -96,57 +93,101 @@ 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 { 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 } diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 0bea1fed143b0..407239269a8b7 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -184,9 +184,14 @@ func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.S // opts.ProjectBoardID == 0 means all project boards, // 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.ProjectBoardID == db.NoConditionID { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 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})) } return sess } diff --git a/models/project/issue.go b/models/project/issue.go index ebc9719de55d0..b6025c1c44cf5 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -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, board.ProjectID) if err != nil { return err } diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 031745dd2fcc1..bcc2b02d2ae80 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_id"` ProjectBoardID int64 `json:"project_board_id"` PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 510b4060b293a..e43f9d3ad2c04 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -89,9 +89,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 + var projectIDs []int64 + for _, project := range issue.Projects { + projectIDs = append(projectIDs, project.ID) } return &internal.IndexerData{ @@ -106,7 +106,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 439fdf644bb6a..1aa557d814e9e 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -461,14 +461,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 } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 94300da868330..3e6d8aa2a4a4c 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1244,7 +1244,7 @@ func NewIssuePost(ctx *context.Context) { ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") return } - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { + if err := issues_model.ChangeProjectAssign(ctx, 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 6417024f8ba3c..8d311a342fd6a 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -385,14 +385,10 @@ 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 } diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 4be1f52dd5c69..785fe3d536839 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -151,10 +151,9 @@ {{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}} -
- {{ctx.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 e0d2e102e5ef7..53c3057074b97 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 "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 08fe21190ac8a..6470fb09e9cee 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -232,6 +232,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');