summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnbraten <anton@ju60.de>2021-12-08 07:57:18 +0100
committerGitHub <noreply@github.com>2021-12-08 14:57:18 +0800
commit0ff18a808c7c14d42ea2325b5d9623f7a30d9107 (patch)
tree6271529776d63a8c741bc3159cd5cfacd5539be1
parent4cbe792562e69e76df07cfa4aa9c0c254b2dec7c (diff)
downloadgitea-0ff18a808c7c14d42ea2325b5d9623f7a30d9107.tar.gz
gitea-0ff18a808c7c14d42ea2325b5d9623f7a30d9107.zip
Support sorting for project board issuses (#17152)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
-rw-r--r--models/issue.go2
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v203.go18
-rw-r--r--models/project_board.go2
-rw-r--r--models/project_issue.go49
-rw-r--r--routers/web/repo/projects.go56
-rw-r--r--routers/web/web.go2
-rw-r--r--web_src/js/features/repo-projects.js40
8 files changed, 114 insertions, 57 deletions
diff --git a/models/issue.go b/models/issue.go
index 324f4eaa83..58649c754a 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1219,6 +1219,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64
"ELSE issue.deadline_unix END DESC")
case "priorityrepo":
sess.OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(priorityRepoID, 10) + " THEN 1 ELSE 2 END, issue.created_unix DESC")
+ case "project-column-sorting":
+ sess.Asc("project_issue.sorting")
default:
sess.Desc("issue.created_unix")
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 6b7caba897..a5bacd0d92 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -359,6 +359,8 @@ var migrations = []Migration{
NewMigration("Drop table remote_version (if exists)", dropTableRemoteVersion),
// v202 -> v203
NewMigration("Create key/value table for user settings", createUserSettingsTable),
+ // v203 -> v204
+ NewMigration("Add Sorting to ProjectIssue table", addProjectIssueSorting),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v203.go b/models/migrations/v203.go
new file mode 100644
index 0000000000..2e1dd7289a
--- /dev/null
+++ b/models/migrations/v203.go
@@ -0,0 +1,18 @@
+// Copyright 2021 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 migrations
+
+import (
+ "xorm.io/xorm"
+)
+
+func addProjectIssueSorting(x *xorm.Engine) error {
+ // ProjectIssue saves relation from issue to a project
+ type ProjectIssue struct {
+ Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
+ }
+
+ return x.Sync2(new(ProjectIssue))
+}
diff --git a/models/project_board.go b/models/project_board.go
index 2d422a203b..d40cfd06f0 100644
--- a/models/project_board.go
+++ b/models/project_board.go
@@ -265,6 +265,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) {
issues, err := Issues(&IssuesOptions{
ProjectBoardID: b.ID,
ProjectID: b.ProjectID,
+ SortType: "project-column-sorting",
})
if err != nil {
return nil, err
@@ -276,6 +277,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) {
issues, err := Issues(&IssuesOptions{
ProjectBoardID: -1, // Issues without ProjectBoardID
ProjectID: b.ProjectID,
+ SortType: "project-column-sorting",
})
if err != nil {
return nil, err
diff --git a/models/project_issue.go b/models/project_issue.go
index fb08efa994..c1421485b0 100644
--- a/models/project_issue.go
+++ b/models/project_issue.go
@@ -20,6 +20,7 @@ type ProjectIssue struct {
// If 0, then it has not been added to a specific board in the project
ProjectBoardID int64 `xorm:"INDEX"`
+ Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
}
func init() {
@@ -184,34 +185,34 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U
// |_| |_| \___// |\___|\___|\__|____/ \___/ \__,_|_| \__,_|
// |__/
-// MoveIssueAcrossProjectBoards move a card from one board to another
-func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard) error {
- ctx, committer, err := db.TxContext()
- if err != nil {
- return err
- }
- defer committer.Close()
- sess := db.GetEngine(ctx)
-
- var pis ProjectIssue
- has, err := sess.Where("issue_id=?", issue.ID).Get(&pis)
- if err != nil {
- return err
- }
-
- if !has {
- return fmt.Errorf("issue has to be added to a project first")
- }
+// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
+func MoveIssuesOnProjectBoard(board *ProjectBoard, sortedIssueIDs map[int64]int64) error {
+ return db.WithTx(func(ctx context.Context) error {
+ sess := db.GetEngine(ctx)
- pis.ProjectBoardID = board.ID
- if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil {
- return err
- }
+ issueIDs := make([]int64, 0, len(sortedIssueIDs))
+ for _, issueID := range sortedIssueIDs {
+ issueIDs = append(issueIDs, issueID)
+ }
+ count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
+ if err != nil {
+ return err
+ }
+ if int(count) != len(sortedIssueIDs) {
+ return fmt.Errorf("all issues have to be added to a project first")
+ }
- return committer.Commit()
+ for sorting, issueID := range sortedIssueIDs {
+ _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
}
func (pb *ProjectBoard) removeIssues(e db.Engine) error {
- _, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", pb.ID)
+ _, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0, sorting = 0 WHERE project_board_id = ? ", pb.ID)
return err
}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index c237544385..a8b2a7a5c4 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -5,6 +5,7 @@
package repo
import (
+ "encoding/json"
"fmt"
"net/http"
"net/url"
@@ -299,7 +300,6 @@ func ViewProject(ctx *context.Context) {
ctx.ServerError("LoadIssuesOfBoards", err)
return
}
- ctx.Data["Issues"] = issueList
linkedPrsMap := make(map[int64][]*models.Issue)
for _, issue := range issueList {
@@ -547,9 +547,8 @@ func SetDefaultProjectBoard(ctx *context.Context) {
})
}
-// MoveIssueAcrossBoards move a card from one board to another in a project
-func MoveIssueAcrossBoards(ctx *context.Context) {
-
+// MoveIssues moves or keeps issues in a column and sorts them inside that column
+func MoveIssues(ctx *context.Context) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
@@ -564,59 +563,80 @@ func MoveIssueAcrossBoards(ctx *context.Context) {
return
}
- p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+ project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound("ProjectNotExist", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
- if p.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("", nil)
+ if project.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("InvalidRepoID", nil)
return
}
var board *models.ProjectBoard
if ctx.ParamsInt64(":boardID") == 0 {
-
board = &models.ProjectBoard{
ID: 0,
- ProjectID: 0,
+ ProjectID: project.ID,
Title: ctx.Tr("repo.projects.type.uncategorized"),
}
-
} else {
+ // column
board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
if models.IsErrProjectBoardNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound("ProjectBoardNotExist", nil)
} else {
ctx.ServerError("GetProjectBoard", err)
}
return
}
- if board.ProjectID != p.ID {
- ctx.NotFound("", nil)
+ if board.ProjectID != project.ID {
+ ctx.NotFound("BoardNotInProject", nil)
return
}
}
- issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
+ type movedIssuesForm struct {
+ Issues []struct {
+ IssueID int64 `json:"issueID"`
+ Sorting int64 `json:"sorting"`
+ } `json:"issues"`
+ }
+
+ form := &movedIssuesForm{}
+ if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
+ ctx.ServerError("DecodeMovedIssuesForm", err)
+ }
+
+ issueIDs := make([]int64, 0, len(form.Issues))
+ sortedIssueIDs := make(map[int64]int64)
+ for _, issue := range form.Issues {
+ issueIDs = append(issueIDs, issue.IssueID)
+ sortedIssueIDs[issue.Sorting] = issue.IssueID
+ }
+ movedIssues, err := models.GetIssuesByIDs(issueIDs)
if err != nil {
if models.IsErrIssueNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound("IssueNotExisting", nil)
} else {
ctx.ServerError("GetIssueByID", err)
}
+ return
+ }
+ if len(movedIssues) != len(form.Issues) {
+ ctx.ServerError("IssuesNotFound", err)
return
}
- if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
- ctx.ServerError("MoveIssueAcrossProjectBoards", err)
+ if err = models.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil {
+ ctx.ServerError("MoveIssuesOnProjectBoard", err)
return
}
diff --git a/routers/web/web.go b/routers/web/web.go
index c52d3483f0..0d4d3bd90f 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -897,7 +897,7 @@ func RegisterRoutes(m *web.Route) {
m.Delete("", repo.DeleteProjectBoard)
m.Post("/default", repo.SetDefaultProjectBoard)
- m.Post("/{index}", repo.MoveIssueAcrossBoards)
+ m.Post("/move", repo.MoveIssues)
})
})
}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 986ada2956..5b3f54f8a6 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -1,5 +1,29 @@
const {csrfToken} = window.config;
+function moveIssue({item, from, to, oldIndex}) {
+ const columnCards = to.getElementsByClassName('board-card');
+
+ const columnSorting = {
+ issues: [...columnCards].map((card, i) => ({
+ issueID: parseInt($(card).attr('data-issue')),
+ sorting: i
+ }))
+ };
+
+ $.ajax({
+ url: `${to.getAttribute('data-url')}/move`,
+ data: JSON.stringify(columnSorting),
+ headers: {
+ 'X-Csrf-Token': csrfToken,
+ },
+ contentType: 'application/json',
+ type: 'POST',
+ error: () => {
+ from.insertBefore(item, from.children[oldIndex]);
+ }
+ });
+}
+
async function initRepoProjectSortable() {
const els = document.querySelectorAll('#project-board > .board');
if (!els.length) return;
@@ -40,20 +64,8 @@ async function initRepoProjectSortable() {
group: 'shared',
animation: 150,
ghostClass: 'card-ghost',
- onAdd: ({item, from, to, oldIndex}) => {
- const url = to.getAttribute('data-url');
- const issue = item.getAttribute('data-issue');
- $.ajax(`${url}/${issue}`, {
- headers: {
- 'X-Csrf-Token': csrfToken,
- },
- contentType: 'application/json',
- type: 'POST',
- error: () => {
- from.insertBefore(item, from.children[oldIndex]);
- },
- });
- },
+ onAdd: moveIssue,
+ onUpdate: moveIssue,
});
}
}