Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>tags/v1.16.0-rc1
@@ -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") | |||
} |
@@ -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 |
@@ -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)) | |||
} |
@@ -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 |
@@ -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 | |||
} |
@@ -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 | |||
} | |||
@@ -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()) |
@@ -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, | |||
}); | |||
} | |||
} |