mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Support sorting for project board issuses (#17152)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										18
									
								
								models/migrations/v203.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models/migrations/v203.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| // 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) | ||||
|  | ||||
| 	var pis ProjectIssue | ||||
| 	has, err := sess.Where("issue_id=?", issue.ID).Get(&pis) | ||||
| 		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 !has { | ||||
| 		return fmt.Errorf("issue has to be added to a project first") | ||||
| 		if int(count) != len(sortedIssueIDs) { | ||||
| 			return fmt.Errorf("all issues have to be added to a project first") | ||||
| 		} | ||||
|  | ||||
| 	pis.ProjectBoardID = board.ID | ||||
| 	if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil { | ||||
| 		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 committer.Commit() | ||||
| 		} | ||||
| 		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 err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil { | ||||
| 		ctx.ServerError("MoveIssueAcrossProjectBoards", err) | ||||
| 	if len(movedIssues) != len(form.Issues) { | ||||
| 		ctx.ServerError("IssuesNotFound", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	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, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Anbraten
					Anbraten