Skip to content

Commit df5261d

Browse files
tyroneyehcolonelpanic8
authored andcommitted
Support multiple projects
1 parent 481e738 commit df5261d

File tree

11 files changed

+155
-83
lines changed

11 files changed

+155
-83
lines changed

models/issues/issue.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ type Issue struct {
102102
PosterID int64 `xorm:"INDEX"`
103103
Poster *user_model.User `xorm:"-"`
104104
OriginalAuthor string
105-
OriginalAuthorID int64 `xorm:"index"`
106-
Title string `xorm:"name"`
107-
Content string `xorm:"LONGTEXT"`
108-
RenderedContent string `xorm:"-"`
109-
Labels []*Label `xorm:"-"`
110-
MilestoneID int64 `xorm:"INDEX"`
111-
Milestone *Milestone `xorm:"-"`
112-
Project *project_model.Project `xorm:"-"`
105+
OriginalAuthorID int64 `xorm:"index"`
106+
Title string `xorm:"name"`
107+
Content string `xorm:"LONGTEXT"`
108+
RenderedContent string `xorm:"-"`
109+
Labels []*Label `xorm:"-"`
110+
MilestoneID int64 `xorm:"INDEX"`
111+
Milestone *Milestone `xorm:"-"`
112+
Projects []*project_model.Project `xorm:"-"`
113113
Priority int
114114
AssigneeID int64 `xorm:"-"`
115115
Assignee *user_model.User `xorm:"-"`

models/issues/issue_list.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,14 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
263263
}
264264

265265
for _, issue := range issues {
266-
issue.Project = projectMaps[issue.ID]
266+
projectIDs := issue.ProjectIDs()
267+
for _, i := range projectIDs {
268+
if projectMaps[i] != nil {
269+
issue.Projects = append(issue.Projects, projectMaps[i])
270+
}
271+
}
267272
}
273+
268274
return nil
269275
}
270276

models/issues/issue_project.go

Lines changed: 87 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,24 @@ import (
1414

1515
// LoadProject load the project the issue was assigned to
1616
func (issue *Issue) LoadProject(ctx context.Context) (err error) {
17-
if issue.Project == nil {
18-
var p project_model.Project
19-
has, err := db.GetEngine(ctx).Table("project").
17+
if issue.Projects == nil {
18+
err = db.GetEngine(ctx).Table("project").
2019
Join("INNER", "project_issue", "project.id=project_issue.project_id").
21-
Where("project_issue.issue_id = ?", issue.ID).Get(&p)
22-
if err != nil {
23-
return err
24-
} else if has {
25-
issue.Project = &p
26-
}
20+
Where("project_issue.issue_id = ?", issue.ID).OrderBy("title").
21+
Find(&issue.Projects)
2722
}
2823
return err
2924
}
3025

31-
func (issue *Issue) projectID(ctx context.Context) int64 {
32-
var ip project_model.ProjectIssue
33-
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
34-
if err != nil || !has {
35-
return 0
26+
27+
func (issue *Issue) projectIDs(ctx context.Context) []int64 {
28+
var ips []int64
29+
if err := db.GetEngine(ctx).Table("project_issue").Select("project_id").
30+
Where("issue_id=?", issue.ID).Find(&ips); err != nil {
31+
return nil
3632
}
37-
return ip.ProjectID
33+
34+
return ips
3835
}
3936

4037
// 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
9693
}
9794

9895
// ChangeProjectAssign changes the project associated with an issue
99-
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
96+
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64, action string) error {
10097
ctx, committer, err := db.TxContext(ctx)
10198
if err != nil {
10299
return err
103100
}
104101
defer committer.Close()
105102

106-
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
103+
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID, action); err != nil {
107104
return err
108105
}
109106

110107
return committer.Commit()
111108
}
112109

113-
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
114-
oldProjectID := issue.projectID(ctx)
115-
110+
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64, action string) error {
116111
if err := issue.LoadRepo(ctx); err != nil {
117112
return err
118113
}
119114

120-
// Only check if we add a new project and not remove it.
121-
if newProjectID > 0 {
122-
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
123-
if err != nil {
124-
return err
115+
oldProjectIDs := issue.projectIDs(ctx)
116+
117+
if len(oldProjectIDs) > 0 {
118+
for _, i := range oldProjectIDs {
119+
// Only check if we add a new project and not remove it.
120+
if newProjectID > 0 {
121+
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
122+
if err != nil {
123+
return err
124+
}
125+
if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID {
126+
return fmt.Errorf("issue's repository is not the same as project's repository")
127+
}
128+
}
129+
130+
if action == "attach" && newProjectID > 0 {
131+
if err := db.Insert(ctx, &project_model.ProjectIssue{
132+
IssueID: issue.ID,
133+
ProjectID: newProjectID,
134+
}); err != nil {
135+
return err
136+
}
137+
i = 0
138+
} else {
139+
if action == "clear" {
140+
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
141+
return err
142+
}
143+
} else {
144+
i = newProjectID
145+
newProjectID = 0
146+
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=? AND project_issue.project_id=?", issue.ID, i).Delete(&project_model.ProjectIssue{}); err != nil {
147+
return err
148+
}
149+
}
150+
}
151+
152+
if i > 0 || newProjectID > 0 {
153+
if _, err := CreateComment(ctx, &CreateCommentOptions{
154+
Type: CommentTypeProject,
155+
Doer: doer,
156+
Repo: issue.Repo,
157+
Issue: issue,
158+
OldProjectID: i,
159+
ProjectID: newProjectID,
160+
}); err != nil {
161+
return err
162+
}
163+
}
164+
if action != "clear" && newProjectID == 0 || newProjectID > 0 {
165+
break
166+
}
125167
}
126-
if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID {
127-
return fmt.Errorf("issue's repository is not the same as project's repository")
168+
} else {
169+
if action == "attach" || action == "" {
170+
if err := db.Insert(ctx, &project_model.ProjectIssue{
171+
IssueID: issue.ID,
172+
ProjectID: newProjectID,
173+
}); err != nil {
174+
return err
175+
}
128176
}
129-
}
130-
131-
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
132-
return err
133-
}
134177

135-
if oldProjectID > 0 || newProjectID > 0 {
136-
if _, err := CreateComment(ctx, &CreateCommentOptions{
137-
Type: CommentTypeProject,
138-
Doer: doer,
139-
Repo: issue.Repo,
140-
Issue: issue,
141-
OldProjectID: oldProjectID,
142-
ProjectID: newProjectID,
143-
}); err != nil {
144-
return err
178+
if newProjectID > 0 {
179+
if _, err := CreateComment(ctx, &CreateCommentOptions{
180+
Type: CommentTypeProject,
181+
Doer: doer,
182+
Repo: issue.Repo,
183+
Issue: issue,
184+
OldProjectID: 0,
185+
ProjectID: newProjectID,
186+
}); err != nil {
187+
return err
188+
}
145189
}
146190
}
147191

148-
return db.Insert(ctx, &project_model.ProjectIssue{
149-
IssueID: issue.ID,
150-
ProjectID: newProjectID,
151-
})
192+
return nil
152193
}

models/issues/issue_search.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,14 @@ func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.S
184184
// opts.ProjectBoardID == 0 means all project boards,
185185
// do not need to apply any condition
186186
if opts.ProjectBoardID > 0 {
187-
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID}))
188-
} else if opts.ProjectBoardID == db.NoConditionID {
189-
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
187+
sess.In("issue.id", builder.Select("issue_id").From("project_issue").
188+
Where(builder.Eq{"project_board_id": opts.ProjectBoardID}))
189+
} else if opts.ProjectID > 0 {
190+
sess.In("issue.id", builder.Select("issue_id").From("project_issue").
191+
Where(builder.Eq{"project_board_id": 0, "project_id": opts.ProjectID}))
192+
} else {
193+
sess.In("issue.id", builder.Select("issue_id").From("project_issue").
194+
Where(builder.Eq{"project_board_id": 0}))
190195
}
191196
return sess
192197
}

models/project/issue.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
9393
}
9494

9595
for sorting, issueID := range sortedIssueIDs {
96-
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
96+
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=? AND project_id=?", board.ID, sorting, issueID, board.ProjectID)
9797
if err != nil {
9898
return err
9999
}

routers/web/org/projects.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -461,14 +461,9 @@ func UpdateIssueProject(ctx *context.Context) {
461461
}
462462

463463
projectID := ctx.FormInt64("id")
464+
action := ctx.FormString("action")
464465
for _, issue := range issues {
465-
if issue.Project != nil {
466-
if issue.Project.ID == projectID {
467-
continue
468-
}
469-
}
470-
471-
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
466+
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, action); err != nil {
472467
ctx.ServerError("ChangeProjectAssign", err)
473468
return
474469
}

routers/web/repo/issue.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
574574
Page: -1,
575575
IsClosed: util.OptionalBoolFalse,
576576
Type: project_model.TypeRepository,
577+
SortType: "title",
577578
})
578579
if err != nil {
579580
ctx.ServerError("GetProjects", err)
@@ -584,6 +585,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
584585
Page: -1,
585586
IsClosed: util.OptionalBoolFalse,
586587
Type: repoOwnerType,
588+
SortType: "title",
587589
})
588590
if err != nil {
589591
ctx.ServerError("GetProjects", err)
@@ -1244,7 +1246,7 @@ func NewIssuePost(ctx *context.Context) {
12441246
ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
12451247
return
12461248
}
1247-
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
1249+
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, ctx.FormString("action")); err != nil {
12481250
ctx.ServerError("ChangeProjectAssign", err)
12491251
return
12501252
}

routers/web/repo/projects.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,14 +385,15 @@ func UpdateIssueProject(ctx *context.Context) {
385385
}
386386

387387
projectID := ctx.FormInt64("id")
388+
action := ctx.FormString("action")
388389
for _, issue := range issues {
389390
if issue.Project != nil {
390391
if issue.Project.ID == projectID {
391392
continue
392393
}
393394
}
394395

395-
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
396+
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, action); err != nil {
396397
ctx.ServerError("ChangeProjectAssign", err)
397398
return
398399
}

templates/repo/issue/view_content/sidebar.tmpl

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,9 @@
151151

152152
{{if .IsProjectsEnabled}}
153153
<div class="divider"></div>
154-
155-
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-project dropdown">
154+
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-projects dropdown">
156155
<a class="text muted flex-text-block">
157-
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
156+
<strong>{{ctx.locale.Tr "repo.issues.new.projects"}}</strong>
158157
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
159158
{{svg "octicon-gear" 16 "gt-ml-2"}}
160159
{{end}}
@@ -173,9 +172,19 @@
173172
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
174173
</div>
175174
{{range .OpenProjects}}
176-
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
175+
{{$ProjectID := .ID}}
176+
{{$checked := false}}
177+
{{range $.Issue.Projects}}
178+
{{if eq .ID $ProjectID}}
179+
{{$checked = true}}
180+
{{end}}
181+
{{end}}
182+
<a class="item muted sidebar-item-link{{if $checked}} checked{{end}}" href="#" data-id="{{.ID}}" data-href="{{.Link ctx}}">
183+
<span class="octicon-check {{if not $checked}}invisible{{end}}">{{svg "octicon-check"}}</span>
184+
<span class="text">
177185
{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
178-
</a>
186+
</span>
187+
</a>
179188
{{end}}
180189
{{end}}
181190
{{if .ClosedProjects}}
@@ -184,21 +193,33 @@
184193
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
185194
</div>
186195
{{range .ClosedProjects}}
187-
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
196+
{{$ProjectID := .ID}}
197+
{{$checked := false}}
198+
{{range $.Issue.Projects}}
199+
{{if eq .ID $ProjectID}}
200+
{{$checked = true}}
201+
{{end}}
202+
{{end}}
203+
<a class="item muted sidebar-item-link{{if $checked}} checked{{end}}" href="#" data-id="{{.ID}}" data-href="{{.Link ctx}}">
204+
<span class="octicon-check {{if not $checked}}invisible{{end}}">{{svg "octicon-check"}}</span>
205+
<span class="text">
188206
{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
207+
</span>
189208
</a>
190209
{{end}}
191210
{{end}}
192211
</div>
193212
</div>
194-
<div class="ui select-project list">
195-
<span class="no-select item {{if .Issue.Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
213+
<div class="ui projects list">
214+
<span class="no-select item {{if .Issue.Projects}}gt-hidden{{end}}">{{.locale.Tr "repo.issues.new.no_projects"}}</span>
196215
<div class="selected">
197-
{{if .Issue.Project}}
198-
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
199-
{{svg .Issue.Project.IconName 18 "gt-mr-3"}}{{.Issue.Project.Title}}
216+
{{range .Issue.Projects}}
217+
<div class="item">
218+
<a class="item muted sidebar-item-link" href="{{$.RepoLink}}/projects/{{.ID}}">
219+
{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
200220
</a>
201-
{{end}}
221+
</div>
222+
{{end}}
202223
</div>
203224
</div>
204225
{{end}}

templates/shared/issuelist.tmpl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@
9292
{{svg "octicon-milestone" 14}}{{.Milestone.Name}}
9393
</a>
9494
{{end}}
95-
{{if .Project}}
96-
<a class="project flex-text-inline" href="{{.Project.Link ctx}}">
97-
{{svg .Project.IconName 14}}{{.Project.Title}}
95+
{{range .Projects}}
96+
<a class="project flex-text-inline" href="{{.Link ctx}}">
97+
{{svg .IconName 14 "gt-mr-2"}}{{.Title}}
9898
</a>
9999
{{end}}
100100
{{if .Ref}}

web_src/js/features/repo-legacy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export function initRepoCommentForm() {
232232

233233
// Init labels and assignees
234234
initListSubmits('select-label', 'labels');
235+
initListSubmits('select-projects', 'projects');
235236
initListSubmits('select-assignees', 'assignees');
236237
initListSubmits('select-assignees-modify', 'assignees');
237238
initListSubmits('select-reviewers-modify', 'assignees');

0 commit comments

Comments
 (0)