From c69c01d2b6b08a89448b5596fd2233fa4e802ac3 Mon Sep 17 00:00:00 2001
From: Romain <romdum@users.noreply.github.com>
Date: Thu, 11 Feb 2021 17:32:27 +0100
Subject: [PATCH] Sort / Move project boards (#14634)

Sort Project board (#14533)
---
 models/project_board.go           | 34 +++++++++++++++++++++++++++++-----
 modules/forms/repo_form.go        |  8 ++++----
 routers/repo/projects.go          | 12 ++++++++----
 routers/routes/web.go             |  4 ++--
 templates/repo/projects/view.tmpl |  2 +-
 web_src/js/features/projects.js   | 29 +++++++++++++++++++++++++++++
 6 files changed, 73 insertions(+), 16 deletions(-)

diff --git a/models/project_board.go b/models/project_board.go
index a9c0b3ed8b..e56bf8f819 100644
--- a/models/project_board.go
+++ b/models/project_board.go
@@ -36,6 +36,7 @@ type ProjectBoard struct {
 	ID      int64 `xorm:"pk autoincr"`
 	Title   string
 	Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
+	Sorting int8 `xorm:"DEFAULT 0"`
 
 	ProjectID int64 `xorm:"INDEX NOT NULL"`
 	CreatorID int64 `xorm:"NOT NULL"`
@@ -157,15 +158,24 @@ func getProjectBoard(e Engine, boardID int64) (*ProjectBoard, error) {
 	return board, nil
 }
 
-// UpdateProjectBoard updates the title of a project board
+// UpdateProjectBoard updates a project board
 func UpdateProjectBoard(board *ProjectBoard) error {
 	return updateProjectBoard(x, board)
 }
 
 func updateProjectBoard(e Engine, board *ProjectBoard) error {
-	_, err := e.ID(board.ID).Cols(
-		"title",
-	).Update(board)
+	var fieldToUpdate []string
+
+	if board.Sorting != 0 {
+		fieldToUpdate = append(fieldToUpdate, "sorting")
+	}
+
+	if board.Title != "" {
+		fieldToUpdate = append(fieldToUpdate, "title")
+	}
+
+	_, err := e.ID(board.ID).Cols(fieldToUpdate...).Update(board)
+
 	return err
 }
 
@@ -178,7 +188,7 @@ func GetProjectBoards(projectID int64) (ProjectBoardList, error) {
 func getProjectBoards(e Engine, projectID int64) ([]*ProjectBoard, error) {
 	var boards = make([]*ProjectBoard, 0, 5)
 
-	if err := e.Where("project_id=? AND `default`=?", projectID, false).Find(&boards); err != nil {
+	if err := e.Where("project_id=? AND `default`=?", projectID, false).OrderBy("Sorting").Find(&boards); err != nil {
 		return nil, err
 	}
 
@@ -277,3 +287,17 @@ func (bs ProjectBoardList) LoadIssues() (IssueList, error) {
 	}
 	return issues, nil
 }
+
+// UpdateProjectBoardSorting update project board sorting
+func UpdateProjectBoardSorting(bs ProjectBoardList) error {
+	for i := range bs {
+		_, err := x.ID(bs[i].ID).Cols(
+			"sorting",
+		).Update(bs[i])
+
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/modules/forms/repo_form.go b/modules/forms/repo_form.go
index ac968a1dd5..f177b21f05 100644
--- a/modules/forms/repo_form.go
+++ b/modules/forms/repo_form.go
@@ -487,10 +487,10 @@ type UserCreateProjectForm struct {
 	UID       int64 `binding:"Required"`
 }
 
-// EditProjectBoardTitleForm is a form for editing the title of a project's
-// board
-type EditProjectBoardTitleForm struct {
-	Title string `binding:"Required;MaxSize(100)"`
+// EditProjectBoardForm is a form for editing a project board
+type EditProjectBoardForm struct {
+	Title   string `binding:"Required;MaxSize(100)"`
+	Sorting int8
 }
 
 //    _____  .__.__                   __
diff --git a/routers/repo/projects.go b/routers/repo/projects.go
index 49bcfef0ce..4aa03e9efc 100644
--- a/routers/repo/projects.go
+++ b/routers/repo/projects.go
@@ -403,7 +403,7 @@ func DeleteProjectBoard(ctx *context.Context) {
 
 // AddBoardToProjectPost allows a new board to be added to a project.
 func AddBoardToProjectPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*auth.EditProjectBoardTitleForm)
+	form := web.GetForm(ctx).(*auth.EditProjectBoardForm)
 	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.",
@@ -481,9 +481,9 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project,
 	return project, board
 }
 
-// EditProjectBoardTitle allows a project board's title to be updated
-func EditProjectBoardTitle(ctx *context.Context) {
-	form := web.GetForm(ctx).(*auth.EditProjectBoardTitleForm)
+// EditProjectBoard allows a project board's to be updated
+func EditProjectBoard(ctx *context.Context) {
+	form := web.GetForm(ctx).(*auth.EditProjectBoardForm)
 	_, board := checkProjectBoardChangePermissions(ctx)
 	if ctx.Written() {
 		return
@@ -493,6 +493,10 @@ func EditProjectBoardTitle(ctx *context.Context) {
 		board.Title = form.Title
 	}
 
+	if form.Sorting != 0 {
+		board.Sorting = form.Sorting
+	}
+
 	if err := models.UpdateProjectBoard(board); err != nil {
 		ctx.ServerError("UpdateProjectBoard", err)
 		return
diff --git a/routers/routes/web.go b/routers/routes/web.go
index 1f860a6239..9e3e690fb9 100644
--- a/routers/routes/web.go
+++ b/routers/routes/web.go
@@ -853,7 +853,7 @@ func RegisterRoutes(m *web.Route) {
 				m.Get("/new", repo.NewProject)
 				m.Post("/new", bindIgnErr(auth.CreateProjectForm{}), repo.NewProjectPost)
 				m.Group("/{id}", func() {
-					m.Post("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.AddBoardToProjectPost)
+					m.Post("", bindIgnErr(auth.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
 					m.Post("/delete", repo.DeleteProject)
 
 					m.Get("/edit", repo.EditProject)
@@ -861,7 +861,7 @@ func RegisterRoutes(m *web.Route) {
 					m.Post("/{action:open|close}", repo.ChangeProjectStatus)
 
 					m.Group("/{boardID}", func() {
-						m.Put("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.EditProjectBoardTitle)
+						m.Put("", bindIgnErr(auth.EditProjectBoardForm{}), repo.EditProjectBoard)
 						m.Delete("", repo.DeleteProjectBoard)
 						m.Post("/default", repo.SetDefaultProjectBoard)
 
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index 9b2aa4bc7d..de1fc37b03 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -72,7 +72,7 @@
 		<div class="board">
 			{{ range $board := .Boards }}
 
-			<div class="ui segment board-column">
+			<div class="ui segment board-column" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}">
 				<div class="board-column-header">
 					<div class="ui large label board-label">{{.Title}}</div>
 					{{if and $.CanWriteProjects (not $.Repository.IsArchived) $.PageIsProjects (ne .ID 0)}}
diff --git a/web_src/js/features/projects.js b/web_src/js/features/projects.js
index b5f52f7443..254079b769 100644
--- a/web_src/js/features/projects.js
+++ b/web_src/js/features/projects.js
@@ -8,6 +8,34 @@ export default async function initProject() {
   const {Sortable} = await import(/* webpackChunkName: "sortable" */'sortablejs');
   const boardColumns = document.getElementsByClassName('board-column');
 
+  new Sortable(
+    document.getElementsByClassName('board')[0],
+    {
+      group: 'board-column',
+      draggable: '.board-column',
+      animation: 150,
+      onSort: () => {
+        const board = document.getElementsByClassName('board')[0];
+        const boardColumns = board.getElementsByClassName('board-column');
+
+        boardColumns.forEach((column, i) => {
+          if (parseInt($(column).data('sorting')) !== i) {
+            $.ajax({
+              url: $(column).data('url'),
+              data: JSON.stringify({sorting: i}),
+              headers: {
+                'X-Csrf-Token': csrf,
+                'X-Remote': true,
+              },
+              contentType: 'application/json',
+              method: 'PUT',
+            });
+          }
+        });
+      },
+    },
+  );
+
   for (const column of boardColumns) {
     new Sortable(
       column.getElementsByClassName('board')[0],
@@ -74,6 +102,7 @@ export default async function initProject() {
 
     window.location.reload();
   });
+
   $('.delete-project-board').each(function () {
     $(this).click(function (e) {
       e.preventDefault();