Skip to content

Commit

Permalink
KanBan: be able to set default board (#14147)
Browse files Browse the repository at this point in the history
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: zeripath <art27@cantab.net>
  • Loading branch information
3 people authored Jan 15, 2021
1 parent c09e11d commit 3091600
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 55 deletions.
95 changes: 75 additions & 20 deletions models/project_board.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/builder"
"xorm.io/xorm"
)

Expand Down Expand Up @@ -164,45 +165,99 @@ func UpdateProjectBoard(board *ProjectBoard) error {
func updateProjectBoard(e Engine, board *ProjectBoard) error {
_, err := e.ID(board.ID).Cols(
"title",
"default",
).Update(board)
return err
}

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

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

sess := x.Where("project_id=?", projectID)
return boards, sess.Find(&boards)
if err := e.Where("project_id=? AND `default`=?", projectID, false).Find(&boards); err != nil {
return nil, err
}

defaultB, err := getDefaultBoard(e, projectID)
if err != nil {
return nil, err
}

return append([]*ProjectBoard{defaultB}, boards...), nil
}

// GetUncategorizedBoard represents a board for issues not assigned to one
func GetUncategorizedBoard(projectID int64) (*ProjectBoard, error) {
// getDefaultBoard return default board and create a dummy if none exist
func getDefaultBoard(e Engine, projectID int64) (*ProjectBoard, error) {
var board ProjectBoard
exist, err := e.Where("project_id=? AND `default`=?", projectID, true).Get(&board)
if err != nil {
return nil, err
}
if exist {
return &board, nil
}

// represents a board for issues not assigned to one
return &ProjectBoard{
ProjectID: projectID,
Title: "Uncategorized",
Default: true,
}, nil
}

// SetDefaultBoard represents a board for issues not assigned to one
// if boardID is 0 unset default
func SetDefaultBoard(projectID, boardID int64) error {
sess := x

_, err := sess.Where(builder.Eq{
"project_id": projectID,
"`default`": true,
}).Cols("`default`").Update(&ProjectBoard{Default: false})
if err != nil {
return err
}

if boardID > 0 {
_, err = sess.ID(boardID).Where(builder.Eq{"project_id": projectID}).
Cols("`default`").Update(&ProjectBoard{Default: true})
}

return err
}

// LoadIssues load issues assigned to this board
func (b *ProjectBoard) LoadIssues() (IssueList, error) {
var boardID int64
if !b.Default {
boardID = b.ID

} else {
// Issues without ProjectBoardID
boardID = -1
}
issues, err := Issues(&IssuesOptions{
ProjectBoardID: boardID,
ProjectID: b.ProjectID,
})
b.Issues = issues
return issues, err
issueList := make([]*Issue, 0, 10)

if b.ID != 0 {
issues, err := Issues(&IssuesOptions{
ProjectBoardID: b.ID,
ProjectID: b.ProjectID,
})
if err != nil {
return nil, err
}
issueList = issues
}

if b.Default {
issues, err := Issues(&IssuesOptions{
ProjectBoardID: -1, // Issues without ProjectBoardID
ProjectID: b.ProjectID,
})
if err != nil {
return nil, err
}
issueList = append(issueList, issues...)
}

b.Issues = issueList
return issueList, nil
}

// LoadIssues load issues assigned to the boards
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,8 @@ projects.board.edit_title = "New Board Name"
projects.board.new_title = "New Board Name"
projects.board.new_submit = "Submit"
projects.board.new = "New Board"
projects.board.set_default = "Set Default"
projects.board.set_default_desc = "Set this board as default for uncategorized issues and pulls"
projects.board.delete = "Delete Board"
projects.board.deletion_desc = "Deleting a project board moves all related issues to 'Uncategorized'. Continue?"
projects.open = Open
Expand Down
58 changes: 39 additions & 19 deletions routers/repo/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,23 +270,17 @@ func ViewProject(ctx *context.Context) {
return
}

uncategorizedBoard, err := models.GetUncategorizedBoard(project.ID)
uncategorizedBoard.Title = ctx.Tr("repo.projects.type.uncategorized")
if err != nil {
ctx.ServerError("GetUncategorizedBoard", err)
return
}

boards, err := models.GetProjectBoards(project.ID)
if err != nil {
ctx.ServerError("GetProjectBoards", err)
return
}

allBoards := models.ProjectBoardList{uncategorizedBoard}
allBoards = append(allBoards, boards...)
if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
}

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

ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["Project"] = project
ctx.Data["Boards"] = allBoards
ctx.Data["Boards"] = boards
ctx.Data["PageIsProjects"] = true
ctx.Data["RequiresDraggable"] = true

Expand Down Expand Up @@ -416,21 +410,19 @@ func AddBoardToProjectPost(ctx *context.Context, form auth.EditProjectBoardTitle
})
}

// EditProjectBoardTitle allows a project board's title to be updated
func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitleForm) {

func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
if ctx.User == nil {
ctx.JSON(403, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
return nil, nil
}

if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(403, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
return nil, nil
}

project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
Expand All @@ -440,25 +432,35 @@ func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitle
} else {
ctx.ServerError("GetProjectByID", err)
}
return
return nil, nil
}

board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return
return nil, nil
}
if board.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(422, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
})
return
return nil, nil
}

if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(422, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
})
return nil, nil
}
return project, board
}

// EditProjectBoardTitle allows a project board's title to be updated
func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitleForm) {

_, board := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}

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

// SetDefaultProjectBoard set default board for uncategorized issues/pulls
func SetDefaultProjectBoard(ctx *context.Context) {

project, board := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}

if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
ctx.ServerError("SetDefaultBoard", err)
return
}

ctx.JSON(200, map[string]interface{}{
"ok": true,
})
}

// MoveIssueAcrossBoards move a card from one board to another in a project
func MoveIssueAcrossBoards(ctx *context.Context) {

Expand Down
28 changes: 28 additions & 0 deletions routers/repo/projects_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package repo

import (
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/test"

"github.com/stretchr/testify/assert"
)

func TestCheckProjectBoardChangePermissions(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/projects/1/2")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
ctx.SetParams(":id", "1")
ctx.SetParams(":boardID", "2")

project, board := checkProjectBoardChangePermissions(ctx)
assert.NotNil(t, project)
assert.NotNil(t, board)
assert.False(t, ctx.Written())
}
1 change: 1 addition & 0 deletions routers/routes/macaron.go
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
m.Group("/:boardID", func() {
m.Put("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.EditProjectBoardTitle)
m.Delete("", repo.DeleteProjectBoard)
m.Post("/default", repo.SetDefaultProjectBoard)

m.Post("/:index", repo.MoveIssueAcrossBoards)
})
Expand Down
40 changes: 28 additions & 12 deletions templates/repo/projects/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
{{svg "octicon-pencil"}}
{{$.i18n.Tr "repo.projects.board.edit"}}
</a>
{{if not .Default}}
<a class="item show-modal button" data-modal="#set-default-project-board-modal-{{.ID}}">
{{svg "octicon-pin"}}
{{$.i18n.Tr "repo.projects.board.set_default"}}
</a>
{{end}}
<a class="item show-modal button" data-modal="#delete-board-modal-{{.ID}}">
{{svg "octicon-trashcan"}}
{{$.i18n.Tr "repo.projects.board.delete"}}
Expand All @@ -109,24 +115,34 @@
</div>
</div>

<div class="ui basic modal" id="set-default-project-board-modal-{{.ID}}">
<div class="ui icon header">
{{$.i18n.Tr "repo.projects.board.set_default"}}
</div>
<div class="content center">
<label>
{{$.i18n.Tr "repo.projects.board.set_default_desc"}}
</label>
</div>
<div class="text right actions">
<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
<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>
</div>
</div>

<div class="ui basic modal" id="delete-board-modal-{{.ID}}">
<div class="ui icon header">
{{$.i18n.Tr "repo.projects.board.delete"}}
</div>
<div class="content center">
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{$.i18n.Tr "repo.projects.board.deletion_desc"}}
</label>
</div>
<label>
{{$.i18n.Tr "repo.projects.board.deletion_desc"}}
</label>
</div>
<div class="text right actions">
<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
<button class="ui red button delete-project-board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}">{{$.i18n.Tr "repo.projects.board.delete"}}</button>
</div>
<form class="ui form" method="post">
<div class="text right actions">
<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
<button class="ui red button delete-project-board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}">{{$.i18n.Tr "repo.projects.board.delete"}}</button>
</div>
</form>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 3091600

Please sign in to comment.