Skip to content

Commit 3091600

Browse files
6543silverwindzeripath
authored
KanBan: be able to set default board (#14147)
Co-authored-by: silverwind <[email protected]> Co-authored-by: zeripath <[email protected]>
1 parent c09e11d commit 3091600

File tree

7 files changed

+192
-55
lines changed

7 files changed

+192
-55
lines changed

models/project_board.go

+75-20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"code.gitea.io/gitea/modules/setting"
99
"code.gitea.io/gitea/modules/timeutil"
1010

11+
"xorm.io/builder"
1112
"xorm.io/xorm"
1213
)
1314

@@ -164,45 +165,99 @@ func UpdateProjectBoard(board *ProjectBoard) error {
164165
func updateProjectBoard(e Engine, board *ProjectBoard) error {
165166
_, err := e.ID(board.ID).Cols(
166167
"title",
167-
"default",
168168
).Update(board)
169169
return err
170170
}
171171

172172
// GetProjectBoards fetches all boards related to a project
173-
func GetProjectBoards(projectID int64) ([]*ProjectBoard, error) {
173+
// if no default board set, first board is a temporary "Uncategorized" board
174+
func GetProjectBoards(projectID int64) (ProjectBoardList, error) {
175+
return getProjectBoards(x, projectID)
176+
}
174177

178+
func getProjectBoards(e Engine, projectID int64) ([]*ProjectBoard, error) {
175179
var boards = make([]*ProjectBoard, 0, 5)
176180

177-
sess := x.Where("project_id=?", projectID)
178-
return boards, sess.Find(&boards)
181+
if err := e.Where("project_id=? AND `default`=?", projectID, false).Find(&boards); err != nil {
182+
return nil, err
183+
}
184+
185+
defaultB, err := getDefaultBoard(e, projectID)
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
return append([]*ProjectBoard{defaultB}, boards...), nil
179191
}
180192

181-
// GetUncategorizedBoard represents a board for issues not assigned to one
182-
func GetUncategorizedBoard(projectID int64) (*ProjectBoard, error) {
193+
// getDefaultBoard return default board and create a dummy if none exist
194+
func getDefaultBoard(e Engine, projectID int64) (*ProjectBoard, error) {
195+
var board ProjectBoard
196+
exist, err := e.Where("project_id=? AND `default`=?", projectID, true).Get(&board)
197+
if err != nil {
198+
return nil, err
199+
}
200+
if exist {
201+
return &board, nil
202+
}
203+
204+
// represents a board for issues not assigned to one
183205
return &ProjectBoard{
184206
ProjectID: projectID,
185207
Title: "Uncategorized",
186208
Default: true,
187209
}, nil
188210
}
189211

212+
// SetDefaultBoard represents a board for issues not assigned to one
213+
// if boardID is 0 unset default
214+
func SetDefaultBoard(projectID, boardID int64) error {
215+
sess := x
216+
217+
_, err := sess.Where(builder.Eq{
218+
"project_id": projectID,
219+
"`default`": true,
220+
}).Cols("`default`").Update(&ProjectBoard{Default: false})
221+
if err != nil {
222+
return err
223+
}
224+
225+
if boardID > 0 {
226+
_, err = sess.ID(boardID).Where(builder.Eq{"project_id": projectID}).
227+
Cols("`default`").Update(&ProjectBoard{Default: true})
228+
}
229+
230+
return err
231+
}
232+
190233
// LoadIssues load issues assigned to this board
191234
func (b *ProjectBoard) LoadIssues() (IssueList, error) {
192-
var boardID int64
193-
if !b.Default {
194-
boardID = b.ID
195-
196-
} else {
197-
// Issues without ProjectBoardID
198-
boardID = -1
199-
}
200-
issues, err := Issues(&IssuesOptions{
201-
ProjectBoardID: boardID,
202-
ProjectID: b.ProjectID,
203-
})
204-
b.Issues = issues
205-
return issues, err
235+
issueList := make([]*Issue, 0, 10)
236+
237+
if b.ID != 0 {
238+
issues, err := Issues(&IssuesOptions{
239+
ProjectBoardID: b.ID,
240+
ProjectID: b.ProjectID,
241+
})
242+
if err != nil {
243+
return nil, err
244+
}
245+
issueList = issues
246+
}
247+
248+
if b.Default {
249+
issues, err := Issues(&IssuesOptions{
250+
ProjectBoardID: -1, // Issues without ProjectBoardID
251+
ProjectID: b.ProjectID,
252+
})
253+
if err != nil {
254+
return nil, err
255+
}
256+
issueList = append(issueList, issues...)
257+
}
258+
259+
b.Issues = issueList
260+
return issueList, nil
206261
}
207262

208263
// LoadIssues load issues assigned to the boards

options/locale/locale_en-US.ini

+2
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,8 @@ projects.board.edit_title = "New Board Name"
945945
projects.board.new_title = "New Board Name"
946946
projects.board.new_submit = "Submit"
947947
projects.board.new = "New Board"
948+
projects.board.set_default = "Set Default"
949+
projects.board.set_default_desc = "Set this board as default for uncategorized issues and pulls"
948950
projects.board.delete = "Delete Board"
949951
projects.board.deletion_desc = "Deleting a project board moves all related issues to 'Uncategorized'. Continue?"
950952
projects.open = Open

routers/repo/projects.go

+39-19
Original file line numberDiff line numberDiff line change
@@ -270,23 +270,17 @@ func ViewProject(ctx *context.Context) {
270270
return
271271
}
272272

273-
uncategorizedBoard, err := models.GetUncategorizedBoard(project.ID)
274-
uncategorizedBoard.Title = ctx.Tr("repo.projects.type.uncategorized")
275-
if err != nil {
276-
ctx.ServerError("GetUncategorizedBoard", err)
277-
return
278-
}
279-
280273
boards, err := models.GetProjectBoards(project.ID)
281274
if err != nil {
282275
ctx.ServerError("GetProjectBoards", err)
283276
return
284277
}
285278

286-
allBoards := models.ProjectBoardList{uncategorizedBoard}
287-
allBoards = append(allBoards, boards...)
279+
if boards[0].ID == 0 {
280+
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
281+
}
288282

289-
if ctx.Data["Issues"], err = allBoards.LoadIssues(); err != nil {
283+
if ctx.Data["Issues"], err = boards.LoadIssues(); err != nil {
290284
ctx.ServerError("LoadIssuesOfBoards", err)
291285
return
292286
}
@@ -295,7 +289,7 @@ func ViewProject(ctx *context.Context) {
295289

296290
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
297291
ctx.Data["Project"] = project
298-
ctx.Data["Boards"] = allBoards
292+
ctx.Data["Boards"] = boards
299293
ctx.Data["PageIsProjects"] = true
300294
ctx.Data["RequiresDraggable"] = true
301295

@@ -416,21 +410,19 @@ func AddBoardToProjectPost(ctx *context.Context, form auth.EditProjectBoardTitle
416410
})
417411
}
418412

419-
// EditProjectBoardTitle allows a project board's title to be updated
420-
func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitleForm) {
421-
413+
func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
422414
if ctx.User == nil {
423415
ctx.JSON(403, map[string]string{
424416
"message": "Only signed in users are allowed to perform this action.",
425417
})
426-
return
418+
return nil, nil
427419
}
428420

429421
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
430422
ctx.JSON(403, map[string]string{
431423
"message": "Only authorized users are allowed to perform this action.",
432424
})
433-
return
425+
return nil, nil
434426
}
435427

436428
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
@@ -440,25 +432,35 @@ func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitle
440432
} else {
441433
ctx.ServerError("GetProjectByID", err)
442434
}
443-
return
435+
return nil, nil
444436
}
445437

446438
board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
447439
if err != nil {
448440
ctx.ServerError("GetProjectBoard", err)
449-
return
441+
return nil, nil
450442
}
451443
if board.ProjectID != ctx.ParamsInt64(":id") {
452444
ctx.JSON(422, map[string]string{
453445
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
454446
})
455-
return
447+
return nil, nil
456448
}
457449

458450
if project.RepoID != ctx.Repo.Repository.ID {
459451
ctx.JSON(422, map[string]string{
460452
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
461453
})
454+
return nil, nil
455+
}
456+
return project, board
457+
}
458+
459+
// EditProjectBoardTitle allows a project board's title to be updated
460+
func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitleForm) {
461+
462+
_, board := checkProjectBoardChangePermissions(ctx)
463+
if ctx.Written() {
462464
return
463465
}
464466

@@ -476,6 +478,24 @@ func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitle
476478
})
477479
}
478480

481+
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
482+
func SetDefaultProjectBoard(ctx *context.Context) {
483+
484+
project, board := checkProjectBoardChangePermissions(ctx)
485+
if ctx.Written() {
486+
return
487+
}
488+
489+
if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
490+
ctx.ServerError("SetDefaultBoard", err)
491+
return
492+
}
493+
494+
ctx.JSON(200, map[string]interface{}{
495+
"ok": true,
496+
})
497+
}
498+
479499
// MoveIssueAcrossBoards move a card from one board to another in a project
480500
func MoveIssueAcrossBoards(ctx *context.Context) {
481501

routers/repo/projects_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package repo
6+
7+
import (
8+
"testing"
9+
10+
"code.gitea.io/gitea/models"
11+
"code.gitea.io/gitea/modules/test"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestCheckProjectBoardChangePermissions(t *testing.T) {
17+
models.PrepareTestEnv(t)
18+
ctx := test.MockContext(t, "user2/repo1/projects/1/2")
19+
test.LoadUser(t, ctx, 2)
20+
test.LoadRepo(t, ctx, 1)
21+
ctx.SetParams(":id", "1")
22+
ctx.SetParams(":boardID", "2")
23+
24+
project, board := checkProjectBoardChangePermissions(ctx)
25+
assert.NotNil(t, project)
26+
assert.NotNil(t, board)
27+
assert.False(t, ctx.Written())
28+
}

routers/routes/macaron.go

+1
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
800800
m.Group("/:boardID", func() {
801801
m.Put("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.EditProjectBoardTitle)
802802
m.Delete("", repo.DeleteProjectBoard)
803+
m.Post("/default", repo.SetDefaultProjectBoard)
803804

804805
m.Post("/:index", repo.MoveIssueAcrossBoards)
805806
})

templates/repo/projects/view.tmpl

+28-12
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
{{svg "octicon-pencil"}}
8686
{{$.i18n.Tr "repo.projects.board.edit"}}
8787
</a>
88+
{{if not .Default}}
89+
<a class="item show-modal button" data-modal="#set-default-project-board-modal-{{.ID}}">
90+
{{svg "octicon-pin"}}
91+
{{$.i18n.Tr "repo.projects.board.set_default"}}
92+
</a>
93+
{{end}}
8894
<a class="item show-modal button" data-modal="#delete-board-modal-{{.ID}}">
8995
{{svg "octicon-trashcan"}}
9096
{{$.i18n.Tr "repo.projects.board.delete"}}
@@ -109,24 +115,34 @@
109115
</div>
110116
</div>
111117

118+
<div class="ui basic modal" id="set-default-project-board-modal-{{.ID}}">
119+
<div class="ui icon header">
120+
{{$.i18n.Tr "repo.projects.board.set_default"}}
121+
</div>
122+
<div class="content center">
123+
<label>
124+
{{$.i18n.Tr "repo.projects.board.set_default_desc"}}
125+
</label>
126+
</div>
127+
<div class="text right actions">
128+
<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
129+
<button class="ui red button set-default-project-board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}/default">{{$.i18n.Tr "repo.projects.board.set_default"}}</button>
130+
</div>
131+
</div>
132+
112133
<div class="ui basic modal" id="delete-board-modal-{{.ID}}">
113134
<div class="ui icon header">
114135
{{$.i18n.Tr "repo.projects.board.delete"}}
115136
</div>
116137
<div class="content center">
117-
<input type="hidden" name="action" value="delete">
118-
<div class="field">
119-
<label>
120-
{{$.i18n.Tr "repo.projects.board.deletion_desc"}}
121-
</label>
122-
</div>
138+
<label>
139+
{{$.i18n.Tr "repo.projects.board.deletion_desc"}}
140+
</label>
141+
</div>
142+
<div class="text right actions">
143+
<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
144+
<button class="ui red button delete-project-board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}">{{$.i18n.Tr "repo.projects.board.delete"}}</button>
123145
</div>
124-
<form class="ui form" method="post">
125-
<div class="text right actions">
126-
<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
127-
<button class="ui red button delete-project-board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}">{{$.i18n.Tr "repo.projects.board.delete"}}</button>
128-
</div>
129-
</form>
130146
</div>
131147
</div>
132148
</div>

0 commit comments

Comments
 (0)