summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author6543 <6543@obermui.de>2021-01-15 21:29:32 +0100
committerGitHub <noreply@github.com>2021-01-15 22:29:32 +0200
commit3091600cc866bb5236be991e764ad113b8f542a1 (patch)
tree815ee7accac74290faeb8a24f9b08e5bab2f8920
parentc09e11d018c4a92dc83f58e6a6eacd6b085d3328 (diff)
downloadgitea-3091600cc866bb5236be991e764ad113b8f542a1.tar.gz
gitea-3091600cc866bb5236be991e764ad113b8f542a1.zip
KanBan: be able to set default board (#14147)
Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: zeripath <art27@cantab.net>
-rw-r--r--models/project_board.go95
-rw-r--r--options/locale/locale_en-US.ini2
-rw-r--r--routers/repo/projects.go58
-rw-r--r--routers/repo/projects_test.go28
-rw-r--r--routers/routes/macaron.go1
-rw-r--r--templates/repo/projects/view.tmpl40
-rw-r--r--web_src/js/features/projects.js23
7 files changed, 192 insertions, 55 deletions
diff --git a/models/project_board.go b/models/project_board.go
index 260fc8304b..8ffa218377 100644
--- a/models/project_board.go
+++ b/models/project_board.go
@@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
+ "xorm.io/builder"
"xorm.io/xorm"
)
@@ -164,22 +165,43 @@ 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",
@@ -187,22 +209,55 @@ func GetUncategorizedBoard(projectID int64) (*ProjectBoard, error) {
}, 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
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4264d260da..4546a06e81 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -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
diff --git a/routers/repo/projects.go b/routers/repo/projects.go
index 08746aad98..d3cdab6b75 100644
--- a/routers/repo/projects.go
+++ b/routers/repo/projects.go
@@ -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
}
@@ -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
@@ -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"))
@@ -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
}
@@ -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) {
diff --git a/routers/repo/projects_test.go b/routers/repo/projects_test.go
new file mode 100644
index 0000000000..c43cf6d952
--- /dev/null
+++ b/routers/repo/projects_test.go
@@ -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())
+}
diff --git a/routers/routes/macaron.go b/routers/routes/macaron.go
index d331e4ca83..34978724a8 100644
--- a/routers/routes/macaron.go
+++ b/routers/routes/macaron.go
@@ -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)
})
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index b27773f710..a3606c169c 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -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"}}
@@ -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>
diff --git a/web_src/js/features/projects.js b/web_src/js/features/projects.js
index 13318c9f89..b5f52f7443 100644
--- a/web_src/js/features/projects.js
+++ b/web_src/js/features/projects.js
@@ -27,14 +27,14 @@ export default async function initProject() {
},
});
},
- }
+ },
);
}
$('.edit-project-board').each(function () {
const projectTitleLabel = $(this).closest('.board-column-header').find('.board-label');
const projectTitleInput = $(this).find(
- '.content > .form > .field > .project-board-title'
+ '.content > .form > .field > .project-board-title',
);
$(this)
@@ -59,6 +59,21 @@ export default async function initProject() {
});
});
+ $(document).on('click', '.set-default-project-board', async function (e) {
+ e.preventDefault();
+
+ await $.ajax({
+ method: 'POST',
+ url: $(this).data('url'),
+ headers: {
+ 'X-Csrf-Token': csrf,
+ 'X-Remote': true,
+ },
+ contentType: 'application/json',
+ });
+
+ window.location.reload();
+ });
$('.delete-project-board').each(function () {
$(this).click(function (e) {
e.preventDefault();
@@ -72,7 +87,7 @@ export default async function initProject() {
contentType: 'application/json',
method: 'DELETE',
}).done(() => {
- setTimeout(window.location.reload(true), 2000);
+ window.location.reload();
});
});
});
@@ -93,7 +108,7 @@ export default async function initProject() {
method: 'POST',
}).done(() => {
boardTitle.closest('form').removeClass('dirty');
- setTimeout(window.location.reload(true), 2000);
+ window.location.reload();
});
});
}