Skip to content

Commit 0ff18a8

Browse files
anbratenwxiaoguang
andauthoredDec 8, 2021
Support sorting for project board issuses (#17152)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent 4cbe792 commit 0ff18a8

File tree

8 files changed

+114
-57
lines changed

8 files changed

+114
-57
lines changed
 

‎models/issue.go

+2
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64
12191219
"ELSE issue.deadline_unix END DESC")
12201220
case "priorityrepo":
12211221
sess.OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(priorityRepoID, 10) + " THEN 1 ELSE 2 END, issue.created_unix DESC")
1222+
case "project-column-sorting":
1223+
sess.Asc("project_issue.sorting")
12221224
default:
12231225
sess.Desc("issue.created_unix")
12241226
}

‎models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ var migrations = []Migration{
359359
NewMigration("Drop table remote_version (if exists)", dropTableRemoteVersion),
360360
// v202 -> v203
361361
NewMigration("Create key/value table for user settings", createUserSettingsTable),
362+
// v203 -> v204
363+
NewMigration("Add Sorting to ProjectIssue table", addProjectIssueSorting),
362364
}
363365

364366
// GetCurrentDBVersion returns the current db version

‎models/migrations/v203.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
)
10+
11+
func addProjectIssueSorting(x *xorm.Engine) error {
12+
// ProjectIssue saves relation from issue to a project
13+
type ProjectIssue struct {
14+
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
15+
}
16+
17+
return x.Sync2(new(ProjectIssue))
18+
}

‎models/project_board.go

+2
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) {
265265
issues, err := Issues(&IssuesOptions{
266266
ProjectBoardID: b.ID,
267267
ProjectID: b.ProjectID,
268+
SortType: "project-column-sorting",
268269
})
269270
if err != nil {
270271
return nil, err
@@ -276,6 +277,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) {
276277
issues, err := Issues(&IssuesOptions{
277278
ProjectBoardID: -1, // Issues without ProjectBoardID
278279
ProjectID: b.ProjectID,
280+
SortType: "project-column-sorting",
279281
})
280282
if err != nil {
281283
return nil, err

‎models/project_issue.go

+25-24
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type ProjectIssue struct {
2020

2121
// If 0, then it has not been added to a specific board in the project
2222
ProjectBoardID int64 `xorm:"INDEX"`
23+
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
2324
}
2425

2526
func init() {
@@ -184,34 +185,34 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U
184185
// |_| |_| \___// |\___|\___|\__|____/ \___/ \__,_|_| \__,_|
185186
// |__/
186187

187-
// MoveIssueAcrossProjectBoards move a card from one board to another
188-
func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard) error {
189-
ctx, committer, err := db.TxContext()
190-
if err != nil {
191-
return err
192-
}
193-
defer committer.Close()
194-
sess := db.GetEngine(ctx)
195-
196-
var pis ProjectIssue
197-
has, err := sess.Where("issue_id=?", issue.ID).Get(&pis)
198-
if err != nil {
199-
return err
200-
}
201-
202-
if !has {
203-
return fmt.Errorf("issue has to be added to a project first")
204-
}
188+
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
189+
func MoveIssuesOnProjectBoard(board *ProjectBoard, sortedIssueIDs map[int64]int64) error {
190+
return db.WithTx(func(ctx context.Context) error {
191+
sess := db.GetEngine(ctx)
205192

206-
pis.ProjectBoardID = board.ID
207-
if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil {
208-
return err
209-
}
193+
issueIDs := make([]int64, 0, len(sortedIssueIDs))
194+
for _, issueID := range sortedIssueIDs {
195+
issueIDs = append(issueIDs, issueID)
196+
}
197+
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
198+
if err != nil {
199+
return err
200+
}
201+
if int(count) != len(sortedIssueIDs) {
202+
return fmt.Errorf("all issues have to be added to a project first")
203+
}
210204

211-
return committer.Commit()
205+
for sorting, issueID := range sortedIssueIDs {
206+
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
207+
if err != nil {
208+
return err
209+
}
210+
}
211+
return nil
212+
})
212213
}
213214

214215
func (pb *ProjectBoard) removeIssues(e db.Engine) error {
215-
_, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", pb.ID)
216+
_, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0, sorting = 0 WHERE project_board_id = ? ", pb.ID)
216217
return err
217218
}

‎routers/web/repo/projects.go

+38-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package repo
66

77
import (
8+
"encoding/json"
89
"fmt"
910
"net/http"
1011
"net/url"
@@ -299,7 +300,6 @@ func ViewProject(ctx *context.Context) {
299300
ctx.ServerError("LoadIssuesOfBoards", err)
300301
return
301302
}
302-
ctx.Data["Issues"] = issueList
303303

304304
linkedPrsMap := make(map[int64][]*models.Issue)
305305
for _, issue := range issueList {
@@ -547,9 +547,8 @@ func SetDefaultProjectBoard(ctx *context.Context) {
547547
})
548548
}
549549

550-
// MoveIssueAcrossBoards move a card from one board to another in a project
551-
func MoveIssueAcrossBoards(ctx *context.Context) {
552-
550+
// MoveIssues moves or keeps issues in a column and sorts them inside that column
551+
func MoveIssues(ctx *context.Context) {
553552
if ctx.User == nil {
554553
ctx.JSON(http.StatusForbidden, map[string]string{
555554
"message": "Only signed in users are allowed to perform this action.",
@@ -564,59 +563,80 @@ func MoveIssueAcrossBoards(ctx *context.Context) {
564563
return
565564
}
566565

567-
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
566+
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
568567
if err != nil {
569568
if models.IsErrProjectNotExist(err) {
570-
ctx.NotFound("", nil)
569+
ctx.NotFound("ProjectNotExist", nil)
571570
} else {
572571
ctx.ServerError("GetProjectByID", err)
573572
}
574573
return
575574
}
576-
if p.RepoID != ctx.Repo.Repository.ID {
577-
ctx.NotFound("", nil)
575+
if project.RepoID != ctx.Repo.Repository.ID {
576+
ctx.NotFound("InvalidRepoID", nil)
578577
return
579578
}
580579

581580
var board *models.ProjectBoard
582581

583582
if ctx.ParamsInt64(":boardID") == 0 {
584-
585583
board = &models.ProjectBoard{
586584
ID: 0,
587-
ProjectID: 0,
585+
ProjectID: project.ID,
588586
Title: ctx.Tr("repo.projects.type.uncategorized"),
589587
}
590-
591588
} else {
589+
// column
592590
board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
593591
if err != nil {
594592
if models.IsErrProjectBoardNotExist(err) {
595-
ctx.NotFound("", nil)
593+
ctx.NotFound("ProjectBoardNotExist", nil)
596594
} else {
597595
ctx.ServerError("GetProjectBoard", err)
598596
}
599597
return
600598
}
601-
if board.ProjectID != p.ID {
602-
ctx.NotFound("", nil)
599+
if board.ProjectID != project.ID {
600+
ctx.NotFound("BoardNotInProject", nil)
603601
return
604602
}
605603
}
606604

607-
issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
605+
type movedIssuesForm struct {
606+
Issues []struct {
607+
IssueID int64 `json:"issueID"`
608+
Sorting int64 `json:"sorting"`
609+
} `json:"issues"`
610+
}
611+
612+
form := &movedIssuesForm{}
613+
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
614+
ctx.ServerError("DecodeMovedIssuesForm", err)
615+
}
616+
617+
issueIDs := make([]int64, 0, len(form.Issues))
618+
sortedIssueIDs := make(map[int64]int64)
619+
for _, issue := range form.Issues {
620+
issueIDs = append(issueIDs, issue.IssueID)
621+
sortedIssueIDs[issue.Sorting] = issue.IssueID
622+
}
623+
movedIssues, err := models.GetIssuesByIDs(issueIDs)
608624
if err != nil {
609625
if models.IsErrIssueNotExist(err) {
610-
ctx.NotFound("", nil)
626+
ctx.NotFound("IssueNotExisting", nil)
611627
} else {
612628
ctx.ServerError("GetIssueByID", err)
613629
}
630+
return
631+
}
614632

633+
if len(movedIssues) != len(form.Issues) {
634+
ctx.ServerError("IssuesNotFound", err)
615635
return
616636
}
617637

618-
if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
619-
ctx.ServerError("MoveIssueAcrossProjectBoards", err)
638+
if err = models.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil {
639+
ctx.ServerError("MoveIssuesOnProjectBoard", err)
620640
return
621641
}
622642

‎routers/web/web.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ func RegisterRoutes(m *web.Route) {
897897
m.Delete("", repo.DeleteProjectBoard)
898898
m.Post("/default", repo.SetDefaultProjectBoard)
899899

900-
m.Post("/{index}", repo.MoveIssueAcrossBoards)
900+
m.Post("/move", repo.MoveIssues)
901901
})
902902
})
903903
}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())

‎web_src/js/features/repo-projects.js

+26-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
const {csrfToken} = window.config;
22

3+
function moveIssue({item, from, to, oldIndex}) {
4+
const columnCards = to.getElementsByClassName('board-card');
5+
6+
const columnSorting = {
7+
issues: [...columnCards].map((card, i) => ({
8+
issueID: parseInt($(card).attr('data-issue')),
9+
sorting: i
10+
}))
11+
};
12+
13+
$.ajax({
14+
url: `${to.getAttribute('data-url')}/move`,
15+
data: JSON.stringify(columnSorting),
16+
headers: {
17+
'X-Csrf-Token': csrfToken,
18+
},
19+
contentType: 'application/json',
20+
type: 'POST',
21+
error: () => {
22+
from.insertBefore(item, from.children[oldIndex]);
23+
}
24+
});
25+
}
26+
327
async function initRepoProjectSortable() {
428
const els = document.querySelectorAll('#project-board > .board');
529
if (!els.length) return;
@@ -40,20 +64,8 @@ async function initRepoProjectSortable() {
4064
group: 'shared',
4165
animation: 150,
4266
ghostClass: 'card-ghost',
43-
onAdd: ({item, from, to, oldIndex}) => {
44-
const url = to.getAttribute('data-url');
45-
const issue = item.getAttribute('data-issue');
46-
$.ajax(`${url}/${issue}`, {
47-
headers: {
48-
'X-Csrf-Token': csrfToken,
49-
},
50-
contentType: 'application/json',
51-
type: 'POST',
52-
error: () => {
53-
from.insertBefore(item, from.children[oldIndex]);
54-
},
55-
});
56-
},
67+
onAdd: moveIssue,
68+
onUpdate: moveIssue,
5769
});
5870
}
5971
}

0 commit comments

Comments
 (0)
Please sign in to comment.