Changes
8 changed files (+114/-57)
-
-
@@ -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, }); } }
-