From bb95eca07be0c51a262a64ee45e0beedd7be4d93 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 25 Sep 2021 16:16:43 +0200 Subject: [PATCH 01/16] support sorting for project board issuses --- models/issue.go | 2 ++ models/migrations/migrations.go | 2 ++ models/migrations/v196.go | 18 ++++++++++++++++++ models/project_board.go | 2 ++ models/project_issue.go | 10 +++++++--- routers/web/repo/projects.go | 6 +++--- routers/web/web.go | 2 +- web_src/js/features/projects.js | 29 ++++++++++++++++------------- 8 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 models/migrations/v196.go diff --git a/models/issue.go b/models/issue.go index b8c7053b2d2a3..f82ca07cd96d7 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1178,6 +1178,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-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 753ca063d95b5..a8da40d6dedb7 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -344,6 +344,8 @@ var migrations = []Migration{ NewMigration("Add Branch Protection Unprotected Files Column", addBranchProtectionUnprotectedFilesColumn), // v195 -> v196 NewMigration("Add table commit_status_index", addTableCommitStatusIndex), + // v196 -> v197 + NewMigration("Add Sorting to ProjectIssue table", addProjectIssueSorting), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v196.go b/models/migrations/v196.go new file mode 100644 index 0000000000000..2e1dd7289a257 --- /dev/null +++ b/models/migrations/v196.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 6a358685113ba..1b78ce118c34c 100644 --- a/models/project_board.go +++ b/models/project_board.go @@ -250,6 +250,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) { issues, err := Issues(&IssuesOptions{ ProjectBoardID: b.ID, ProjectID: b.ProjectID, + SortType: "project-sorting", }) if err != nil { return nil, err @@ -261,6 +262,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) { issues, err := Issues(&IssuesOptions{ ProjectBoardID: -1, // Issues without ProjectBoardID ProjectID: b.ProjectID, + SortType: "project-sorting", }) if err != nil { return nil, err diff --git a/models/project_issue.go b/models/project_issue.go index a3179507dc9ad..5a4cb4c9d0692 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() { @@ -182,7 +183,7 @@ func addUpdateIssueProject(e *xorm.Session, issue *Issue, doer *User, newProject // |__/ // MoveIssueAcrossProjectBoards move a card from one board to another -func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard) error { +func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard, sorting int64) error { sess := db.NewSession(db.DefaultContext) defer sess.Close() if err := sess.Begin(); err != nil { @@ -200,14 +201,17 @@ func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard) error { } pis.ProjectBoardID = board.ID - if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil { + pis.Sorting = sorting + if _, err := sess.ID(pis.ID).Cols("project_board_id").Cols("sorting").Update(&pis); err != nil { return err } + fmt.Println("sorting", sorting) + return sess.Commit() } 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 556656e5b506f..67a2f748d7054 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -299,7 +299,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 { @@ -587,6 +586,7 @@ func MoveIssueAcrossBoards(ctx *context.Context) { } } else { + // column board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID")) if err != nil { if models.IsErrProjectBoardNotExist(err) { @@ -602,7 +602,7 @@ func MoveIssueAcrossBoards(ctx *context.Context) { } } - issue, err := models.GetIssueByID(ctx.ParamsInt64(":index")) + issue, err := models.GetIssueByID(ctx.ParamsInt64(":issueID")) if err != nil { if models.IsErrIssueNotExist(err) { ctx.NotFound("", nil) @@ -613,7 +613,7 @@ func MoveIssueAcrossBoards(ctx *context.Context) { return } - if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil { + if err := models.MoveIssueAcrossProjectBoards(issue, board, ctx.ParamsInt64(":sorting")); err != nil { ctx.ServerError("MoveIssueAcrossProjectBoards", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index 8d984abcf2ed9..d1e5709dc79e5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -872,7 +872,7 @@ func RegisterRoutes(m *web.Route) { m.Delete("", repo.DeleteProjectBoard) m.Post("/default", repo.SetDefaultProjectBoard) - m.Post("/{index}", repo.MoveIssueAcrossBoards) + m.Post("/{issueID}/{sorting}", repo.MoveIssueAcrossBoards) }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) diff --git a/web_src/js/features/projects.js b/web_src/js/features/projects.js index 0d619cab70ba5..71c64ae3923ee 100644 --- a/web_src/js/features/projects.js +++ b/web_src/js/features/projects.js @@ -37,6 +37,20 @@ export default async function initProject() { }, ); + const moveIssue = (e) => { + $.ajax(`${e.to.dataset.url}/${e.item.dataset.issue}/${e.newIndex}`, { + headers: { + 'X-Csrf-Token': csrf, + 'X-Remote': true, + }, + contentType: 'application/json', + type: 'POST', + error: () => { + e.from.insertBefore(e.item, e.from.children[e.oldIndex]); + }, + }); + }; + for (const column of boardColumns) { new Sortable( column.getElementsByClassName('board')[0], @@ -44,19 +58,8 @@ export default async function initProject() { group: 'shared', animation: 150, ghostClass: 'card-ghost', - onAdd: (e) => { - $.ajax(`${e.to.dataset.url}/${e.item.dataset.issue}`, { - headers: { - 'X-Csrf-Token': csrf, - 'X-Remote': true, - }, - contentType: 'application/json', - type: 'POST', - error: () => { - e.from.insertBefore(e.item, e.from.children[e.oldIndex]); - }, - }); - }, + onAdd: moveIssue, + onUpdate: moveIssue, }, ); } From fbe30c3423c3511a7349eaca5d73f09277a2dd47 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 25 Sep 2021 16:30:31 +0200 Subject: [PATCH 02/16] remove debug --- models/project_issue.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/models/project_issue.go b/models/project_issue.go index 5a4cb4c9d0692..93308ae9d31f3 100644 --- a/models/project_issue.go +++ b/models/project_issue.go @@ -206,8 +206,6 @@ func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard, sorting int return err } - fmt.Println("sorting", sorting) - return sess.Commit() } From 3e9c83a37384a3c59b60fbdea24100787a4f93fd Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 25 Sep 2021 16:33:50 +0200 Subject: [PATCH 03/16] rename issueID to issueIndex --- routers/web/repo/projects.go | 2 +- routers/web/web.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 67a2f748d7054..8decb2226959d 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -602,7 +602,7 @@ func MoveIssueAcrossBoards(ctx *context.Context) { } } - issue, err := models.GetIssueByID(ctx.ParamsInt64(":issueID")) + issue, err := models.GetIssueByID(ctx.ParamsInt64(":issueIndex")) if err != nil { if models.IsErrIssueNotExist(err) { ctx.NotFound("", nil) diff --git a/routers/web/web.go b/routers/web/web.go index d1e5709dc79e5..62fc8e5c6356a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -872,7 +872,7 @@ func RegisterRoutes(m *web.Route) { m.Delete("", repo.DeleteProjectBoard) m.Post("/default", repo.SetDefaultProjectBoard) - m.Post("/{issueID}/{sorting}", repo.MoveIssueAcrossBoards) + m.Post("/{issueIndex}/{sorting}", repo.MoveIssueAcrossBoards) }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) From 65e2d16089a79b21049fd44925d13ad4bd28efc0 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 25 Sep 2021 16:35:08 +0200 Subject: [PATCH 04/16] Revert "rename issueID to issueIndex" This reverts commit 3e9c83a37384a3c59b60fbdea24100787a4f93fd. --- routers/web/repo/projects.go | 2 +- routers/web/web.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 8decb2226959d..67a2f748d7054 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -602,7 +602,7 @@ func MoveIssueAcrossBoards(ctx *context.Context) { } } - issue, err := models.GetIssueByID(ctx.ParamsInt64(":issueIndex")) + issue, err := models.GetIssueByID(ctx.ParamsInt64(":issueID")) if err != nil { if models.IsErrIssueNotExist(err) { ctx.NotFound("", nil) diff --git a/routers/web/web.go b/routers/web/web.go index 62fc8e5c6356a..d1e5709dc79e5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -872,7 +872,7 @@ func RegisterRoutes(m *web.Route) { m.Delete("", repo.DeleteProjectBoard) m.Post("/default", repo.SetDefaultProjectBoard) - m.Post("/{issueIndex}/{sorting}", repo.MoveIssueAcrossBoards) + m.Post("/{issueID}/{sorting}", repo.MoveIssueAcrossBoards) }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) From 208b522f84e8cb7853f230fcabf254336d05e8c2 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 25 Sep 2021 20:08:43 +0200 Subject: [PATCH 05/16] allow sorting project-issue-cards --- web_src/js/features/projects.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/web_src/js/features/projects.js b/web_src/js/features/projects.js index 71c64ae3923ee..708840be84b7d 100644 --- a/web_src/js/features/projects.js +++ b/web_src/js/features/projects.js @@ -15,9 +15,8 @@ export default async function initProject() { draggable: '.board-column', animation: 150, ghostClass: 'card-ghost', - onSort: () => { - const board = document.getElementsByClassName('board')[0]; - const boardColumns = board.getElementsByClassName('board-column'); + onSort: (e) => { + const boardColumns = e.to.getElementsByClassName('board-column'); boardColumns.forEach((column, i) => { if (parseInt($(column).data('sorting')) !== i) { @@ -38,16 +37,21 @@ export default async function initProject() { ); const moveIssue = (e) => { - $.ajax(`${e.to.dataset.url}/${e.item.dataset.issue}/${e.newIndex}`, { - headers: { - 'X-Csrf-Token': csrf, - 'X-Remote': true, - }, - contentType: 'application/json', - type: 'POST', - error: () => { - e.from.insertBefore(e.item, e.from.children[e.oldIndex]); - }, + const boardCards = e.to.getElementsByClassName('board-card'); + + boardCards.forEach((card, i) => { + const id = $(card).data('issue'); + $.ajax(`${e.to.dataset.url}/${id}/${i}`, { + headers: { + 'X-Csrf-Token': csrf, + 'X-Remote': true, + }, + contentType: 'application/json', + type: 'POST', + error: () => { + e.from.insertBefore(e.item, e.from.children[e.oldIndex]); + }, + }); }); }; From 615eb321a7654784e17b8fe68eb350fe9a8bdba4 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 3 Oct 2021 16:10:43 +0200 Subject: [PATCH 06/16] use batch update --- models/project_issue.go | 30 +++++++++++++++-------------- routers/web/repo/projects.go | 33 ++++++++++++++++++++------------ routers/web/web.go | 2 +- services/forms/repo_form.go | 8 ++++++++ web_src/js/features/projects.js | 34 +++++++++++++++++++-------------- 5 files changed, 66 insertions(+), 41 deletions(-) diff --git a/models/project_issue.go b/models/project_issue.go index 93308ae9d31f3..934cfa3df4f2b 100644 --- a/models/project_issue.go +++ b/models/project_issue.go @@ -182,28 +182,30 @@ func addUpdateIssueProject(e *xorm.Session, issue *Issue, doer *User, newProject // |_| |_| \___// |\___|\___|\__|____/ \___/ \__,_|_| \__,_| // |__/ -// MoveIssueAcrossProjectBoards move a card from one board to another -func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard, sorting int64) error { +// MoveIssuesOnProjectBoard moves or keeps issuses in a column and sorts them inside of that column +func MoveIssuesOnProjectBoard(board *ProjectBoard, issues map[int64]*Issue) error { sess := db.NewSession(db.DefaultContext) defer sess.Close() if err := sess.Begin(); err != nil { return err } - var pis ProjectIssue - has, err := sess.Where("issue_id=?", issue.ID).Get(&pis) - if err != nil { - return err - } + for sorting, issue := range issues { + 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") - } + if !has { + return fmt.Errorf("issue has to be added to a project first") + } - pis.ProjectBoardID = board.ID - pis.Sorting = sorting - if _, err := sess.ID(pis.ID).Cols("project_board_id").Cols("sorting").Update(&pis); err != nil { - return err + pis.ProjectBoardID = board.ID + pis.Sorting = sorting + if _, err := sess.ID(pis.ID).Cols("project_board_id").Cols("sorting").Update(&pis); err != nil { + return err + } } return sess.Commit() diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 67a2f748d7054..75602a1226dff 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -544,9 +544,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 issuses in a column and sorts them inside of 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.", @@ -602,19 +601,29 @@ func MoveIssueAcrossBoards(ctx *context.Context) { } } - issue, err := models.GetIssueByID(ctx.ParamsInt64(":issueID")) - if err != nil { - if models.IsErrIssueNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetIssueByID", err) + form := web.GetForm(ctx).(*forms.MoveProjectIssuesForm) + + issues := make(map[int64]*models.Issue) + for _, i := range form.Issues { + issue, err := models.GetIssueByID(i.IssueID) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetIssueByID", err) + } + + return } - return + issues[i.Sorting] = issue } - if err := models.MoveIssueAcrossProjectBoards(issue, board, ctx.ParamsInt64(":sorting")); err != nil { - ctx.ServerError("MoveIssueAcrossProjectBoards", err) + // TODO: fix form is always empty + fmt.Println("uff", issues, form) + + if err := models.MoveIssuesOnProjectBoard(board, issues); err != nil { + ctx.ServerError("MoveIssuesOnProjectBoard", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index d1e5709dc79e5..94fa751ddbce3 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -872,7 +872,7 @@ func RegisterRoutes(m *web.Route) { m.Delete("", repo.DeleteProjectBoard) m.Post("/default", repo.SetDefaultProjectBoard) - m.Post("/{issueID}/{sorting}", repo.MoveIssueAcrossBoards) + m.Post("/move", bindIgnErr(forms.MoveProjectIssuesForm{}), repo.MoveIssues) }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 1210d5dfc5c05..1b608d207d7f5 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -501,6 +501,14 @@ type EditProjectBoardForm struct { Sorting int8 } +// MoveProjectIssuesForm is a form for moving and sorting project-issues in a column +type MoveProjectIssuesForm struct { + Issues map[string]struct { + IssueID int64 `binding:"Required"` + Sorting int64 `binding:"Required"` + } `binding:"Required"` +} + // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ diff --git a/web_src/js/features/projects.js b/web_src/js/features/projects.js index 708840be84b7d..a7293c3f45d40 100644 --- a/web_src/js/features/projects.js +++ b/web_src/js/features/projects.js @@ -37,21 +37,27 @@ export default async function initProject() { ); const moveIssue = (e) => { - const boardCards = e.to.getElementsByClassName('board-card'); + const columnCards = e.to.getElementsByClassName('board-card'); - boardCards.forEach((card, i) => { - const id = $(card).data('issue'); - $.ajax(`${e.to.dataset.url}/${id}/${i}`, { - headers: { - 'X-Csrf-Token': csrf, - 'X-Remote': true, - }, - contentType: 'application/json', - type: 'POST', - error: () => { - e.from.insertBefore(e.item, e.from.children[e.oldIndex]); - }, - }); + const columnSorting = { + issues: [...columnCards].map((card, i) => ({ + issueID: $(card).data('issue'), + sorting: i, + })), + }; + + $.ajax({ + url: `${e.to.dataset.url}/move`, + data: JSON.stringify(columnSorting), + headers: { + 'X-Csrf-Token': csrf, + 'X-Remote': true, + }, + contentType: 'application/json', + type: 'POST', + error: () => { + e.from.insertBefore(e.item, e.from.children[e.oldIndex]); + }, }); }; From 11b8e1db05bac8d88d90ae0d463d3bc7af81f977 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 3 Oct 2021 16:12:19 +0200 Subject: [PATCH 07/16] add migration --- models/migrations/v197.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 models/migrations/v197.go diff --git a/models/migrations/v197.go b/models/migrations/v197.go new file mode 100644 index 0000000000000..2e1dd7289a257 --- /dev/null +++ b/models/migrations/v197.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)) +} From 0384563528805a51e3b6fd9b85a829423af33bc4 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 3 Oct 2021 17:13:32 +0200 Subject: [PATCH 08/16] fix form type --- routers/web/repo/projects.go | 3 --- services/forms/repo_form.go | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index d1176528d69fb..3662b3c1d89e7 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -622,9 +622,6 @@ func MoveIssues(ctx *context.Context) { issues[i.Sorting] = issue } - // TODO: fix form is always empty - fmt.Println("uff", issues, form) - if err := models.MoveIssuesOnProjectBoard(board, issues); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 2aa227e5fae2c..41dbe9c634904 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -504,7 +504,7 @@ type EditProjectBoardForm struct { // MoveProjectIssuesForm is a form for moving and sorting project-issues in a column type MoveProjectIssuesForm struct { - Issues map[string]struct { + Issues []struct { IssueID int64 `binding:"Required"` Sorting int64 `binding:"Required"` } `binding:"Required"` From cd3028a9f0810b6ca563e8582be572f4cc881aeb Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 3 Oct 2021 19:11:04 +0200 Subject: [PATCH 09/16] apply suggestions --- models/issue.go | 2 +- models/migrations/migrations.go | 4 ++-- models/project_board.go | 4 ++-- models/project_issue.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/models/issue.go b/models/issue.go index 81581bb7e51dd..fccf1713a1de7 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1165,7 +1165,7 @@ 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-sorting": + 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 483a5f0c297e1..1a139f1bfdb4e 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -345,9 +345,9 @@ var migrations = []Migration{ // v195 -> v196 NewMigration("Add table commit_status_index", addTableCommitStatusIndex), // v196 -> v197 - NewMigration("Add Sorting to ProjectIssue table", addProjectIssueSorting), - // v197 -> v198 NewMigration("Add Color to ProjectBoard table", addColorColToProjectBoard), + // v197 -> v198 + NewMigration("Add Sorting to ProjectIssue table", addProjectIssueSorting), } // GetCurrentDBVersion returns the current db version diff --git a/models/project_board.go b/models/project_board.go index 53945c7108de7..da9b6e3c3d520 100644 --- a/models/project_board.go +++ b/models/project_board.go @@ -266,7 +266,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) { issues, err := Issues(&IssuesOptions{ ProjectBoardID: b.ID, ProjectID: b.ProjectID, - SortType: "project-sorting", + SortType: "project-column-sorting", }) if err != nil { return nil, err @@ -278,7 +278,7 @@ func (b *ProjectBoard) LoadIssues() (IssueList, error) { issues, err := Issues(&IssuesOptions{ ProjectBoardID: -1, // Issues without ProjectBoardID ProjectID: b.ProjectID, - SortType: "project-sorting", + SortType: "project-column-sorting", }) if err != nil { return nil, err diff --git a/models/project_issue.go b/models/project_issue.go index 934cfa3df4f2b..38d330ff3974c 100644 --- a/models/project_issue.go +++ b/models/project_issue.go @@ -203,7 +203,7 @@ func MoveIssuesOnProjectBoard(board *ProjectBoard, issues map[int64]*Issue) erro pis.ProjectBoardID = board.ID pis.Sorting = sorting - if _, err := sess.ID(pis.ID).Cols("project_board_id").Cols("sorting").Update(&pis); err != nil { + if _, err := sess.ID(pis.ID).Cols("project_board_id", "sorting").Update(&pis); err != nil { return err } } From 402dc9f3ee1f3aed1d03e59048a551ff1eff7210 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Mon, 15 Nov 2021 22:32:42 +0100 Subject: [PATCH 10/16] update sorting in batch --- models/project_issue.go | 30 ++++++++++++++++++++---------- routers/web/repo/projects.go | 35 ++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/models/project_issue.go b/models/project_issue.go index 38d330ff3974c..8e9ea58ad06ab 100644 --- a/models/project_issue.go +++ b/models/project_issue.go @@ -183,26 +183,36 @@ func addUpdateIssueProject(e *xorm.Session, issue *Issue, doer *User, newProject // |__/ // MoveIssuesOnProjectBoard moves or keeps issuses in a column and sorts them inside of that column -func MoveIssuesOnProjectBoard(board *ProjectBoard, issues map[int64]*Issue) error { +func MoveIssuesOnProjectBoard(board *ProjectBoard, issueIDs map[int64]int64) error { sess := db.NewSession(db.DefaultContext) defer sess.Close() if err := sess.Begin(); err != nil { return err } - for sorting, issue := range issues { - var pis ProjectIssue - has, err := sess.Where("issue_id=?", issue.ID).Get(&pis) + ids := make([]int64, len(issueIDs)) + for _, id := range issueIDs { + ids = append(ids, id) + } + count, err := sess.Where("project_board_id=?", board.ID, issueIDs).In("issue_id", ids).Count() + if err != nil { + return err + } + + if int(count) != len(issueIDs) { + return fmt.Errorf("all issues have to be added to a project first") + } + + for sorting, id := range issueIDs { + // var pis ProjectIssue + // pis.IssueID = id + // pis.ProjectBoardID = board.ID + // pis.Sorting = sorting + _, err := sess.Exec("UPDATE `project_issue` SET project_board_id = ?, sorting = ? WHERE issue_id = ?", board.ID, sorting, id) if err != nil { return err } - if !has { - return fmt.Errorf("issue has to be added to a project first") - } - - pis.ProjectBoardID = board.ID - pis.Sorting = sorting if _, err := sess.ID(pis.ID).Cols("project_board_id", "sorting").Update(&pis); err != nil { return err } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 3662b3c1d89e7..95437d4f6ef68 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -606,23 +606,32 @@ func MoveIssues(ctx *context.Context) { form := web.GetForm(ctx).(*forms.MoveProjectIssuesForm) - issues := make(map[int64]*models.Issue) - for _, i := range form.Issues { - issue, err := models.GetIssueByID(i.IssueID) - if err != nil { - if models.IsErrIssueNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetIssueByID", err) - } - - return + issueIDs := make([]int64, len(form.Issues)) + for _, issue := range form.Issues { + issueIDs = append(issueIDs, issue.IssueID) + } + issues, err := models.GetIssuesByIDs(issueIDs) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetIssueByID", err) } - issues[i.Sorting] = issue + return + } + + if len(issues) != len(form.Issues) { + ctx.ServerError("IssusesNotFound", err) + return + } + + sortedIssueIDs := make(map[int64]int64) + for _, i := range form.Issues { + sortedIssueIDs[i.Sorting] = i.Sorting } - if err := models.MoveIssuesOnProjectBoard(board, issues); err != nil { + if err := models.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } From 4b7500037f0b1b94a4a004f685f0185ee362ba18 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Mon, 15 Nov 2021 22:40:27 +0100 Subject: [PATCH 11/16] undo formatting --- web_src/js/features/repo-projects.js | 226 ++++++++++++--------------- 1 file changed, 97 insertions(+), 129 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 92edd9cccac44..fb7a6e2a5c2b1 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -1,186 +1,173 @@ -const { csrfToken } = window.config; +const {csrfToken} = window.config; async function initRepoProjectSortable() { - const { Sortable } = await import( - /* webpackChunkName: "sortable" */ "sortablejs" + 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, + ghostClass: 'card-ghost', + onSort: (e) => { + const boardColumns = e.to.getElementsByClassName('board-column'); + + boardColumns.forEach((column, i) => { + if (parseInt($(column).data('sorting')) !== i) { + $.ajax({ + url: $(column).data('url'), + data: JSON.stringify({sorting: i, color: rgbToHex($(column).css('backgroundColor'))}), + headers: { + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, + }, + contentType: 'application/json', + method: 'PUT', + }); + } + }); + }, + }, ); - const boardColumns = document.getElementsByClassName("board-column"); - - new Sortable(document.getElementsByClassName("board")[0], { - group: "board-column", - draggable: ".board-column", - animation: 150, - ghostClass: "card-ghost", - onSort: e => { - const boardColumns = e.to.getElementsByClassName("board-column"); - - boardColumns.forEach((column, i) => { - if (parseInt($(column).data("sorting")) !== i) { - $.ajax({ - url: $(column).data("url"), - data: JSON.stringify({ - sorting: i, - color: rgbToHex($(column).css("backgroundColor")) - }), - headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true - }, - contentType: "application/json", - method: "PUT" - }); - } - }); - } - }); - const moveIssue = e => { - const columnCards = e.to.getElementsByClassName("board-card"); + const moveIssue = (e) => { + const columnCards = e.to.getElementsByClassName('board-card'); const columnSorting = { issues: [...columnCards].map((card, i) => ({ - issueID: $(card).data("issue"), - sorting: i - })) + issueID: $(card).data('issue'), + sorting: i, + })), }; $.ajax({ url: `${e.to.dataset.url}/move`, data: JSON.stringify(columnSorting), headers: { - "X-Csrf-Token": csrf, - "X-Remote": true + 'X-Csrf-Token': csrf, + 'X-Remote': true, }, - contentType: "application/json", - type: "POST", + contentType: 'application/json', + type: 'POST', error: () => { e.from.insertBefore(e.item, e.from.children[e.oldIndex]); - } + }, }); }; for (const column of boardColumns) { - new Sortable(column.getElementsByClassName("board")[0], { - group: "shared", - animation: 150, - ghostClass: "card-ghost", - onAdd: moveIssue, - onUpdate: moveIssue - }); + new Sortable( + column.getElementsByClassName('board')[0], + { + group: 'shared', + animation: 150, + ghostClass: 'card-ghost', + onAdd: moveIssue, + onUpdate: moveIssue, + }, + ); } } export default function initRepoProject() { - if (!$(".repository.projects").length) { + if (!$('.repository.projects').length) { return; } const _promise = initRepoProjectSortable(); - $(".edit-project-board").each(function() { - const projectHeader = $(this).closest(".board-column-header"); - const projectTitleLabel = projectHeader.find(".board-label"); + $('.edit-project-board').each(function () { + const projectHeader = $(this).closest('.board-column-header'); + const projectTitleLabel = projectHeader.find('.board-label'); const projectTitleInput = $(this).find( - ".content > .form > .field > .project-board-title" - ); - const projectColorInput = $(this).find( - ".content > .form > .field #new_board_color" + '.content > .form > .field > .project-board-title', ); - const boardColumn = $(this).closest(".board-column"); + const projectColorInput = $(this).find('.content > .form > .field #new_board_color'); + const boardColumn = $(this).closest('.board-column'); - if (boardColumn.css("backgroundColor")) { - setLabelColor( - projectHeader, - rgbToHex(boardColumn.css("backgroundColor")) - ); + if (boardColumn.css('backgroundColor')) { + setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor'))); } $(this) - .find(".content > .form > .actions > .red") - .on("click", function(e) { + .find('.content > .form > .actions > .red') + .on('click', function (e) { e.preventDefault(); $.ajax({ - url: $(this).data("url"), - data: JSON.stringify({ - title: projectTitleInput.val(), - color: projectColorInput.val() - }), + url: $(this).data('url'), + data: JSON.stringify({title: projectTitleInput.val(), color: projectColorInput.val()}), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "PUT" + contentType: 'application/json', + method: 'PUT', }).done(() => { projectTitleLabel.text(projectTitleInput.val()); - projectTitleInput.closest("form").removeClass("dirty"); + projectTitleInput.closest('form').removeClass('dirty'); if (projectColorInput.val()) { setLabelColor(projectHeader, projectColorInput.val()); } - boardColumn.attr( - "style", - `background: ${projectColorInput.val()}!important` - ); - $(".ui.modal").modal("hide"); + boardColumn.attr('style', `background: ${projectColorInput.val()}!important`); + $('.ui.modal').modal('hide'); }); }); }); - $(document).on("click", ".set-default-project-board", async function(e) { + $(document).on('click', '.set-default-project-board', async function (e) { e.preventDefault(); await $.ajax({ - method: "POST", - url: $(this).data("url"), + method: 'POST', + url: $(this).data('url'), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json" + contentType: 'application/json', }); window.location.reload(); }); - $(".delete-project-board").each(function() { - $(this).click(function(e) { + $('.delete-project-board').each(function () { + $(this).click(function (e) { e.preventDefault(); $.ajax({ - url: $(this).data("url"), + url: $(this).data('url'), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "DELETE" + contentType: 'application/json', + method: 'DELETE', }).done(() => { window.location.reload(); }); }); }); - $("#new_board_submit").click(function(e) { + $('#new_board_submit').click(function (e) { e.preventDefault(); - const boardTitle = $("#new_board"); - const projectColorInput = $("#new_board_color_picker"); + const boardTitle = $('#new_board'); + const projectColorInput = $('#new_board_color_picker'); $.ajax({ - url: $(this).data("url"), - data: JSON.stringify({ - title: boardTitle.val(), - color: projectColorInput.val() - }), + url: $(this).data('url'), + data: JSON.stringify({title: boardTitle.val(), color: projectColorInput.val()}), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "POST" + contentType: 'application/json', + method: 'POST', }).done(() => { - boardTitle.closest("form").removeClass("dirty"); + boardTitle.closest('form').removeClass('dirty'); window.location.reload(); }); }); @@ -193,9 +180,9 @@ function setLabelColor(label, color) { const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; if (luminance > 0.179) { - label.removeClass("light-label").addClass("dark-label"); + label.removeClass('light-label').addClass('dark-label'); } else { - label.removeClass("dark-label").addClass("light-label"); + label.removeClass('dark-label').addClass('light-label'); } } @@ -213,25 +200,6 @@ function rgbToHex(rgb) { } function hex(x) { - const hexDigits = [ - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "a", - "b", - "c", - "d", - "e", - "f" - ]; - return Number.isNaN(x) - ? "00" - : hexDigits[(x - (x % 16)) / 16] + hexDigits[x % 16]; + const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16]; } From 4f98fe10e14accab9ccc4e45c9e1b5a21ae4d9a6 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 5 Dec 2021 23:01:20 +0100 Subject: [PATCH 12/16] merge --- models/migrations/v203.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 models/migrations/v203.go diff --git a/models/migrations/v203.go b/models/migrations/v203.go new file mode 100644 index 0000000000000..2e1dd7289a257 --- /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)) +} From 9ab2566d9020ee39221296bccfea05c77715b15d Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 5 Dec 2021 23:12:03 +0100 Subject: [PATCH 13/16] undo formatting --- web_src/js/features/repo-projects.js | 175 +++++++++++---------------- 1 file changed, 69 insertions(+), 106 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index a80ebebc0e8f8..27ae1ade5ce24 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -1,42 +1,38 @@ -const { csrfToken } = window.config; +const {csrfToken} = window.config; async function initRepoProjectSortable() { - const els = document.querySelectorAll("#project-board > .board"); + const els = document.querySelectorAll('#project-board > .board'); if (!els.length) return; - const { Sortable } = await import( - /* webpackChunkName: "sortable" */ "sortablejs" - ); + const {Sortable} = await import(/* webpackChunkName: "sortable" */'sortablejs'); + // the HTML layout is: #project-board > .board > .board-column .board.cards > .board-card.card .content const mainBoard = els[0]; - let boardColumns = mainBoard.getElementsByClassName("board-column"); + let boardColumns = mainBoard.getElementsByClassName('board-column'); new Sortable(mainBoard, { - group: "board-column", - draggable: ".board-column", + group: 'board-column', + draggable: '.board-column', filter: '[data-id="0"]', animation: 150, - ghostClass: "card-ghost", + ghostClass: 'card-ghost', onSort: () => { - boardColumns = mainBoard.getElementsByClassName("board-column"); + boardColumns = mainBoard.getElementsByClassName('board-column'); for (let i = 0; i < boardColumns.length; i++) { const column = boardColumns[i]; - if (parseInt($(column).data("sorting")) !== i) { + if (parseInt($(column).data('sorting')) !== i) { $.ajax({ - url: $(column).data("url"), - data: JSON.stringify({ - sorting: i, - color: rgbToHex($(column).css("backgroundColor")) - }), + url: $(column).data('url'), + data: JSON.stringify({sorting: i, color: rgbToHex($(column).css('backgroundColor'))}), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "PUT" + contentType: 'application/json', + method: 'PUT', }); } } - } + }, }); const moveIssue = ({ item, from, to, oldIndex }) => { @@ -64,129 +60,115 @@ async function initRepoProjectSortable() { }); }; - for (const column of boardColumns) { - const boardCardList = boardColumn.getElementsByClassName("board")[0]; + for (const boardColumn of boardColumns) { + const boardCardList = boardColumn.getElementsByClassName('board')[0]; new Sortable(boardCardList, { - group: "shared", + group: 'shared', animation: 150, - ghostClass: "card-ghost", + ghostClass: 'card-ghost', onAdd: moveIssue, - onUpdate: moveIssue + onUpdate: moveIssue, }); } } export default function initRepoProject() { - if (!$(".repository.projects").length) { + if (!$('.repository.projects').length) { return; } const _promise = initRepoProjectSortable(); - $(".edit-project-board").each(function() { - const projectHeader = $(this).closest(".board-column-header"); - const projectTitleLabel = projectHeader.find(".board-label"); + $('.edit-project-board').each(function () { + const projectHeader = $(this).closest('.board-column-header'); + const projectTitleLabel = projectHeader.find('.board-label'); const projectTitleInput = $(this).find( - ".content > .form > .field > .project-board-title" - ); - const projectColorInput = $(this).find( - ".content > .form > .field #new_board_color" + '.content > .form > .field > .project-board-title', ); - const boardColumn = $(this).closest(".board-column"); + const projectColorInput = $(this).find('.content > .form > .field #new_board_color'); + const boardColumn = $(this).closest('.board-column'); - if (boardColumn.css("backgroundColor")) { - setLabelColor( - projectHeader, - rgbToHex(boardColumn.css("backgroundColor")) - ); + if (boardColumn.css('backgroundColor')) { + setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor'))); } $(this) - .find(".content > .form > .actions > .red") - .on("click", function(e) { + .find('.content > .form > .actions > .red') + .on('click', function (e) { e.preventDefault(); $.ajax({ - url: $(this).data("url"), - data: JSON.stringify({ - title: projectTitleInput.val(), - color: projectColorInput.val() - }), + url: $(this).data('url'), + data: JSON.stringify({title: projectTitleInput.val(), color: projectColorInput.val()}), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "PUT" + contentType: 'application/json', + method: 'PUT', }).done(() => { projectTitleLabel.text(projectTitleInput.val()); - projectTitleInput.closest("form").removeClass("dirty"); + projectTitleInput.closest('form').removeClass('dirty'); if (projectColorInput.val()) { setLabelColor(projectHeader, projectColorInput.val()); } - boardColumn.attr( - "style", - `background: ${projectColorInput.val()}!important` - ); - $(".ui.modal").modal("hide"); + boardColumn.attr('style', `background: ${projectColorInput.val()}!important`); + $('.ui.modal').modal('hide'); }); }); }); - $(document).on("click", ".set-default-project-board", async function(e) { + $(document).on('click', '.set-default-project-board', async function (e) { e.preventDefault(); await $.ajax({ - method: "POST", - url: $(this).data("url"), + method: 'POST', + url: $(this).data('url'), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json" + contentType: 'application/json', }); window.location.reload(); }); - $(".delete-project-board").each(function() { - $(this).click(function(e) { + $('.delete-project-board').each(function () { + $(this).click(function (e) { e.preventDefault(); $.ajax({ - url: $(this).data("url"), + url: $(this).data('url'), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "DELETE" + contentType: 'application/json', + method: 'DELETE', }).done(() => { window.location.reload(); }); }); }); - $("#new_board_submit").click(function(e) { + $('#new_board_submit').click(function (e) { e.preventDefault(); - const boardTitle = $("#new_board"); - const projectColorInput = $("#new_board_color_picker"); + const boardTitle = $('#new_board'); + const projectColorInput = $('#new_board_color_picker'); $.ajax({ - url: $(this).data("url"), - data: JSON.stringify({ - title: boardTitle.val(), - color: projectColorInput.val() - }), + url: $(this).data('url'), + data: JSON.stringify({title: boardTitle.val(), color: projectColorInput.val()}), headers: { - "X-Csrf-Token": csrfToken, - "X-Remote": true + 'X-Csrf-Token': csrfToken, + 'X-Remote': true, }, - contentType: "application/json", - method: "POST" + contentType: 'application/json', + method: 'POST', }).done(() => { - boardTitle.closest("form").removeClass("dirty"); + boardTitle.closest('form').removeClass('dirty'); window.location.reload(); }); }); @@ -199,9 +181,9 @@ function setLabelColor(label, color) { const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; if (luminance > 0.179) { - label.removeClass("light-label").addClass("dark-label"); + label.removeClass('light-label').addClass('dark-label'); } else { - label.removeClass("dark-label").addClass("light-label"); + label.removeClass('dark-label').addClass('light-label'); } } @@ -219,25 +201,6 @@ function rgbToHex(rgb) { } function hex(x) { - const hexDigits = [ - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "a", - "b", - "c", - "d", - "e", - "f" - ]; - return Number.isNaN(x) - ? "00" - : hexDigits[(x - (x % 16)) / 16] + hexDigits[x % 16]; + const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16]; } From 69be5939ca4d35dd0d3f869f6d05083aa88bb218 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 5 Dec 2021 23:21:40 +0100 Subject: [PATCH 14/16] fix linter --- web_src/js/features/repo-projects.js | 50 ++++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 27ae1ade5ce24..8ec04d351874a 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -1,5 +1,30 @@ const {csrfToken} = window.config; +function moveIssue({item, from, to, oldIndex}) { + const columnCards = to.getElementsByClassName('board-card'); + + const columnSorting = { + issues: [...columnCards].map((card, i) => ({ + issueID: $(card).data('issue'), + sorting: i + })) + }; + + $.ajax({ + url: `${to.getAttribute('url')}/move`, + data: JSON.stringify(columnSorting), + headers: { + 'X-Csrf-Token': csrfToken, + 'X-Remote': true + }, + 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; @@ -35,31 +60,6 @@ async function initRepoProjectSortable() { }, }); - const moveIssue = ({ item, from, to, oldIndex }) => { - const columnCards = to.getElementsByClassName("board-card"); - - const columnSorting = { - issues: [...columnCards].map((card, i) => ({ - issueID: $(card).data("issue"), - sorting: i - })) - }; - - $.ajax({ - url: `${to.dataset.url}/move`, - data: JSON.stringify(columnSorting), - headers: { - "X-Csrf-Token": csrf, - "X-Remote": true - }, - contentType: "application/json", - type: "POST", - error: () => { - from.insertBefore(item, from.children[oldIndex]); - } - }); - }; - for (const boardColumn of boardColumns) { const boardCardList = boardColumn.getElementsByClassName('board')[0]; new Sortable(boardCardList, { From de2d38a582e8820f4e5e0c3e4a5ca0acdd9b1491 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 6 Dec 2021 13:58:10 +0800 Subject: [PATCH 15/16] fix --- models/project_issue.go | 53 +++++++++++----------------- routers/web/repo/projects.go | 43 ++++++++++++---------- routers/web/web.go | 2 +- services/forms/repo_form.go | 8 ----- web_src/js/features/repo-projects.js | 5 ++- 5 files changed, 48 insertions(+), 63 deletions(-) diff --git a/models/project_issue.go b/models/project_issue.go index a6cb959f4e2d7..c1421485b0a29 100644 --- a/models/project_issue.go +++ b/models/project_issue.go @@ -185,44 +185,31 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U // |_| |_| \___// |\___|\___|\__|____/ \___/ \__,_|_| \__,_| // |__/ -// MoveIssuesOnProjectBoard moves or keeps issuses in a column and sorts them inside of that column -func MoveIssuesOnProjectBoard(board *ProjectBoard, issueIDs map[int64]int64) error { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - ids := make([]int64, len(issueIDs)) - for _, id := range issueIDs { - ids = append(ids, id) - } - count, err := sess.Where("project_board_id=?", board.ID, issueIDs).In("issue_id", ids).Count() - if err != nil { - return err - } - - if int(count) != len(issueIDs) { - return fmt.Errorf("all issues have to be added to a project first") - } - - for sorting, id := range issueIDs { - // var pis ProjectIssue - // pis.IssueID = id - // pis.ProjectBoardID = board.ID - // pis.Sorting = sorting - _, err := sess.Exec("UPDATE `project_issue` SET project_board_id = ?, sorting = ? WHERE issue_id = ?", board.ID, sorting, id) +// 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) + + 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 _, err := sess.ID(pis.ID).Cols("project_board_id", "sorting").Update(&pis); 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 { diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index b35d2bf402306..48de6c7616210 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" @@ -546,7 +547,7 @@ func SetDefaultProjectBoard(ctx *context.Context) { }) } -// MoveIssues moves or keeps issuses in a column and sorts them inside of that column +// 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{ @@ -565,14 +566,14 @@ func MoveIssues(ctx *context.Context) { p, 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) + ctx.NotFound("InvalidRepoID", nil) return } @@ -591,46 +592,52 @@ func MoveIssues(ctx *context.Context) { 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) + ctx.NotFound("BoardNotInProject", nil) return } } - form := web.GetForm(ctx).(*forms.MoveProjectIssuesForm) + 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, len(form.Issues)) + 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 } - issues, err := models.GetIssuesByIDs(issueIDs) + 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(issues) != len(form.Issues) { - ctx.ServerError("IssusesNotFound", err) + if len(movedIssues) != len(form.Issues) { + ctx.ServerError("IssuesNotFound", err) return } - sortedIssueIDs := make(map[int64]int64) - for _, i := range form.Issues { - sortedIssueIDs[i.Sorting] = i.Sorting - } - - if err := models.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { + 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 82329500b39dc..0d4d3bd90f4f9 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("/move", bindIgnErr(forms.MoveProjectIssuesForm{}), repo.MoveIssues) + m.Post("/move", repo.MoveIssues) }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 41dbe9c634904..7c61be5e2221d 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -502,14 +502,6 @@ type EditProjectBoardForm struct { Color string `binding:"MaxSize(7)"` } -// MoveProjectIssuesForm is a form for moving and sorting project-issues in a column -type MoveProjectIssuesForm struct { - Issues []struct { - IssueID int64 `binding:"Required"` - Sorting int64 `binding:"Required"` - } `binding:"Required"` -} - // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 8ec04d351874a..a9a401ee2337f 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -5,17 +5,16 @@ function moveIssue({item, from, to, oldIndex}) { const columnSorting = { issues: [...columnCards].map((card, i) => ({ - issueID: $(card).data('issue'), + issueID: parseInt($(card).attr('data-issue')), sorting: i })) }; $.ajax({ - url: `${to.getAttribute('url')}/move`, + url: `${to.getAttribute('data-url')}/move`, data: JSON.stringify(columnSorting), headers: { 'X-Csrf-Token': csrfToken, - 'X-Remote': true }, contentType: 'application/json', type: 'POST', From c386df74b4765f1cce7e545fb73d2573349d3cd1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 8 Dec 2021 14:13:58 +0800 Subject: [PATCH 16/16] fix uncategorized board --- routers/web/repo/projects.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 48de6c7616210..a8b2a7a5c406b 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -563,7 +563,7 @@ func MoveIssues(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("ProjectNotExist", nil) @@ -572,7 +572,7 @@ func MoveIssues(ctx *context.Context) { } return } - if p.RepoID != ctx.Repo.Repository.ID { + if project.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("InvalidRepoID", nil) return } @@ -580,13 +580,11 @@ func MoveIssues(ctx *context.Context) { 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")) @@ -598,7 +596,7 @@ func MoveIssues(ctx *context.Context) { } return } - if board.ProjectID != p.ID { + if board.ProjectID != project.ID { ctx.NotFound("BoardNotInProject", nil) return }