]> source.dussan.org Git - gitea.git/commitdiff
KanBan: be able to set default board (#14147)
author6543 <6543@obermui.de>
Fri, 15 Jan 2021 20:29:32 +0000 (21:29 +0100)
committerGitHub <noreply@github.com>
Fri, 15 Jan 2021 20:29:32 +0000 (22:29 +0200)
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: zeripath <art27@cantab.net>
models/project_board.go
options/locale/locale_en-US.ini
routers/repo/projects.go
routers/repo/projects_test.go [new file with mode: 0644]
routers/routes/macaron.go
templates/repo/projects/view.tmpl
web_src/js/features/projects.js

index 260fc8304b22e2943d37d96cfe15bdf9db56aaf6..8ffa218377fc3b5914be50f3c563b9f8ab6424b8 100644 (file)
@@ -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
index 4264d260da08bd2fb3098309ae0a8deb0d623b36..4546a06e8180394ecc132d53e2516322cd6760a3 100644 (file)
@@ -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
index 08746aad984af235140741cbadcf4b29e9e1033e..d3cdab6b75d235b4d130f2aa6682c30c61a1e7b8 100644 (file)
@@ -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 (file)
index 0000000..c43cf6d
--- /dev/null
@@ -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())
+}
index d331e4ca83fd66dded3840be0cf65c416cbb78b4..34978724a8368eb3b208a904cb50680f4f928f18 100644 (file)
@@ -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)
                                        })
index b27773f710b063cd1d29bb741a2b8d0823f856f4..a3606c169cdde00d612bdf54241ad87ace5ae7e5 100644 (file)
                                                                        {{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"}}
                                                                        </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>
index 13318c9f89e565f1d92eb48ecec180a5f4cc8203..b5f52f7443891694aa143ab0862d1e28910ef223 100644 (file)
@@ -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();
     });
   });
 }