mirror of
https://github.com/go-gitea/gitea.git
synced 2024-05-09 12:06:10 +02:00
Compare commits
17 Commits
d5d2f5f920
...
a89c587c92
Author | SHA1 | Date | |
---|---|---|---|
Lunny Xiao | a89c587c92 | ||
Kemal Zebari | dd301cae1c | ||
silverwind | 2d078a7f5a | ||
Lunny Xiao | 8229bedc3f | ||
Lunny Xiao | 53aea60bbc | ||
Lunny Xiao | 7bd212afa7 | ||
Lunny Xiao | aeb0bd89a5 | ||
Lunny Xiao | 0613d7f8d5 | ||
Lunny Xiao | 1c67fe6f5f | ||
Lunny Xiao | 0610385dbd | ||
Lunny Xiao | 715ec4cebb | ||
Lunny Xiao | 42bfa04a04 | ||
Lunny Xiao | c041e36715 | ||
Lunny Xiao | 753608a6cf | ||
Lunny Xiao | 75d10316a1 | ||
Lunny Xiao | 50a573f753 | ||
Lunny Xiao | ad1095a5ec |
|
@ -57,6 +57,7 @@ type Engine interface {
|
|||
SumInt(bean any, columnName string) (res int64, err error)
|
||||
Sync(...any) error
|
||||
Select(string) *xorm.Session
|
||||
SetExpr(string, any) *xorm.Session
|
||||
NotIn(string, ...any) *xorm.Session
|
||||
OrderBy(any, ...any) *xorm.Session
|
||||
Exist(...any) (bool, error)
|
||||
|
|
|
@ -90,22 +90,16 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m
|
|||
return issuesMap, nil
|
||||
}
|
||||
|
||||
// ChangeProjectAssign changes the project associated with an issue
|
||||
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
// ChangeProjectAssign changes the project associated with an issue, if newProjectID is 0, the issue is removed from the project
|
||||
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return addUpdateIssueProject(ctx, issue, doer, newProjectID, newColumnID)
|
||||
})
|
||||
}
|
||||
|
||||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
// addUpdateIssueProject adds or updates the project the default column associated with an issue
|
||||
// If newProjectID is 0, the issue is removed from the project
|
||||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
|
||||
oldProjectID := issue.projectID(ctx)
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
|
@ -139,9 +133,25 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U
|
|||
return err
|
||||
}
|
||||
}
|
||||
if newProjectID == 0 || newColumnID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var maxSorting int64
|
||||
if _, err := db.GetEngine(ctx).Select("Max(sorting)").Table("project_issue").
|
||||
Where("project_id=?", newProjectID).
|
||||
And("project_board_id=?", newColumnID).
|
||||
Get(&maxSorting); err != nil {
|
||||
return err
|
||||
}
|
||||
if maxSorting > 0 {
|
||||
maxSorting++
|
||||
}
|
||||
|
||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
ProjectBoardID: newColumnID,
|
||||
Sorting: maxSorting,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -156,6 +156,15 @@ func NewBoard(ctx context.Context, board *Board) error {
|
|||
return fmt.Errorf("bad color code: %s", board.Color)
|
||||
}
|
||||
|
||||
var maxSorting int8
|
||||
if _, err := db.GetEngine(ctx).Select("Max(sorting)").Table("project_board").
|
||||
Where("project_id=?", board.ProjectID).Get(&maxSorting); err != nil {
|
||||
return err
|
||||
}
|
||||
if maxSorting > 0 {
|
||||
board.Sorting = maxSorting
|
||||
}
|
||||
|
||||
_, err := db.GetEngine(ctx).Insert(board)
|
||||
return err
|
||||
}
|
||||
|
@ -189,7 +198,17 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
|
|||
return fmt.Errorf("deleteBoardByID: cannot delete default board")
|
||||
}
|
||||
|
||||
if err = board.removeIssues(ctx); err != nil {
|
||||
// move all issues to the default column
|
||||
project, err := GetProjectByID(ctx, board.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defaultBoard, err := project.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = board.moveIssuesToDefault(ctx, defaultBoard.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -242,21 +261,15 @@ func UpdateBoard(ctx context.Context, board *Board) error {
|
|||
// GetBoards fetches all boards related to a project
|
||||
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
||||
boards := make([]*Board, 0, 5)
|
||||
|
||||
if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting").Find(&boards); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultB, err := p.getDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append([]*Board{defaultB}, boards...), nil
|
||||
return boards, nil
|
||||
}
|
||||
|
||||
// getDefaultBoard return default board and ensure only one exists
|
||||
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
|
||||
// GetDefaultBoard return default board and ensure only one exists
|
||||
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
|
||||
var board Board
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("project_id=? AND `default` = ?", p.ID, true).
|
||||
|
@ -316,3 +329,12 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetColumnsByIDs(ctx context.Context, columnsIDs []int64) (BoardList, error) {
|
||||
columns := make([]*Board, 0, 5)
|
||||
if err := db.GetEngine(ctx).In("id", columnsIDs).OrderBy("sorting").Find(&columns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return columns, nil
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ func TestGetDefaultBoard(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// check if default board was added
|
||||
board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
|
||||
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(5), board.ProjectID)
|
||||
assert.Equal(t, "Uncategorized", board.Title)
|
||||
|
@ -28,7 +28,7 @@ func TestGetDefaultBoard(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// check if multiple defaults were removed
|
||||
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
|
||||
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.Equal(t, int64(9), board.ID)
|
||||
|
|
|
@ -17,7 +17,7 @@ type ProjectIssue struct { //revive:disable-line:exported
|
|||
IssueID int64 `xorm:"INDEX"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
|
||||
// If 0, then it has not been added to a specific board in the project
|
||||
// If this should not be zero from 1.22. If it's zero, it will not be displayed on UI and maybe result in errors.
|
||||
ProjectBoardID int64 `xorm:"INDEX"`
|
||||
|
||||
// the sorting order on the board
|
||||
|
@ -102,7 +102,34 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
|
|||
})
|
||||
}
|
||||
|
||||
func (b *Board) removeIssues(ctx context.Context) error {
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID)
|
||||
func (b *Board) moveIssuesToDefault(ctx context.Context, defaultBoardID int64) error {
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = ? WHERE project_board_id = ? ", defaultBoardID, b.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// MoveColumnsOnProject moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
columnIDs := make([]int64, 0, len(sortedColumnIDs))
|
||||
for _, columnID := range sortedColumnIDs {
|
||||
columnIDs = append(columnIDs, columnID)
|
||||
}
|
||||
count, err := sess.Table(new(Board)).Where("project_id=?", project.ID).In("id", columnIDs).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int(count) != len(sortedColumnIDs) {
|
||||
return fmt.Errorf("all issues have to be added to a project first")
|
||||
}
|
||||
|
||||
for sorting, columnID := range sortedColumnIDs {
|
||||
_, err = sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -372,7 +373,11 @@ func CreatePullReview(ctx *context.APIContext) {
|
|||
// create review and associate all pending review comments
|
||||
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -460,7 +465,11 @@ func SubmitPullReview(ctx *context.APIContext) {
|
|||
// create review and associate all pending review comments
|
||||
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -442,6 +442,21 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
}
|
||||
|
||||
projectID := ctx.FormInt64("id")
|
||||
var dstColumnID int64
|
||||
if projectID > 0 {
|
||||
dstProject, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
return
|
||||
}
|
||||
dstDefaultColumn, err := dstProject.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDefaultBoard", err)
|
||||
return
|
||||
}
|
||||
dstColumnID = dstDefaultColumn.ID
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.Project != nil {
|
||||
if issue.Project.ID == projectID {
|
||||
|
@ -449,7 +464,7 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, dstColumnID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
return
|
||||
}
|
||||
|
@ -678,3 +693,66 @@ func MoveIssues(ctx *context.Context) {
|
|||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// MoveColumns moves or keeps columns in a project and sorts them inside that project
|
||||
func MoveColumns(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound("InvalidRepoID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
type movedColumnsForm struct {
|
||||
Columns []struct {
|
||||
ColumnID int64 `json:"columnID"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
} `json:"columns"`
|
||||
}
|
||||
|
||||
form := &movedColumnsForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedColumnsForm", err)
|
||||
}
|
||||
|
||||
columnIDs := make([]int64, 0, len(form.Columns))
|
||||
sortedColumnIDs := make(map[int64]int64)
|
||||
for _, column := range form.Columns {
|
||||
columnIDs = append(columnIDs, column.ColumnID)
|
||||
sortedColumnIDs[column.Sorting] = column.ColumnID
|
||||
}
|
||||
movedColumns, err := project_model.GetColumnsByIDs(ctx, columnIDs)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetColumnsByIDs", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(movedColumns) != len(form.Columns) {
|
||||
ctx.ServerError("some columns do not exist", errors.New("some columns do not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
for _, column := range movedColumns {
|
||||
if column.ProjectID != project.ID {
|
||||
ctx.ServerError("Some column's projectID is not equal to project's ID", errors.New("Some column's projectID is not equal to project's ID"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
|
||||
ctx.ServerError("MoveColumnsOnProject", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
|
|
@ -385,6 +385,21 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
}
|
||||
|
||||
projectID := ctx.FormInt64("id")
|
||||
var dstColumnID int64
|
||||
if projectID > 0 {
|
||||
dstProject, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
return
|
||||
}
|
||||
dstDefaultColumn, err := dstProject.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDefaultBoard", err)
|
||||
return
|
||||
}
|
||||
dstColumnID = dstDefaultColumn.ID
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.Project != nil {
|
||||
if issue.Project.ID == projectID {
|
||||
|
@ -392,7 +407,7 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, dstColumnID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
return
|
||||
}
|
||||
|
@ -666,3 +681,70 @@ func MoveIssues(ctx *context.Context) {
|
|||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// MoveColumns moves or keeps columns in a project and sorts them inside that project
|
||||
func MoveColumns(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("ProjectNotExist", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if project.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("InvalidRepoID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
type movedColumnsForm struct {
|
||||
Columns []struct {
|
||||
ColumnID int64 `json:"columnID"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
} `json:"columns"`
|
||||
}
|
||||
|
||||
form := &movedColumnsForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedColumnsForm", err)
|
||||
}
|
||||
|
||||
columnIDs := make([]int64, 0, len(form.Columns))
|
||||
sortedColumnIDs := make(map[int64]int64)
|
||||
for _, column := range form.Columns {
|
||||
columnIDs = append(columnIDs, column.ColumnID)
|
||||
sortedColumnIDs[column.Sorting] = column.ColumnID
|
||||
}
|
||||
movedColumns, err := project_model.GetColumnsByIDs(ctx, columnIDs)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetColumnsByIDs", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(movedColumns) != len(form.Columns) {
|
||||
ctx.ServerError("some columns do not exist", errors.New("some columns do not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
for _, column := range movedColumns {
|
||||
if column.ProjectID != project.ID {
|
||||
ctx.ServerError("Some column's projectID is not equal to project's ID", errors.New("Some column's projectID is not equal to project's ID"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
|
||||
ctx.ServerError("MoveColumnsOnProject", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
pull_model "code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
|
@ -1331,10 +1332,20 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||
|
||||
if projectID > 0 {
|
||||
if !ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||
ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects")
|
||||
log.Error("user hasn't the permission to write to projects")
|
||||
return
|
||||
}
|
||||
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil {
|
||||
dstProject, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
return
|
||||
}
|
||||
dstDefaultColumn, err := dstProject.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDefaultBoard", err)
|
||||
return
|
||||
}
|
||||
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID, dstDefaultColumn.ID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -264,6 +264,8 @@ func SubmitReview(ctx *context.Context) {
|
|||
if issues_model.IsContentEmptyErr(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
|
||||
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
|
||||
} else if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.Status(http.StatusUnprocessableEntity)
|
||||
} else {
|
||||
ctx.ServerError("SubmitReview", err)
|
||||
}
|
||||
|
|
|
@ -999,6 +999,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
||||
m.Group("/{id}", func() {
|
||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
|
||||
m.Post("/move", org.MoveColumns)
|
||||
m.Post("/delete", org.DeleteProject)
|
||||
|
||||
m.Get("/edit", org.RenderEditProject)
|
||||
|
@ -1354,6 +1355,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
||||
m.Group("/{id}", func() {
|
||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
|
||||
m.Post("/move", repo.MoveColumns)
|
||||
m.Post("/delete", repo.DeleteProject)
|
||||
|
||||
m.Get("/edit", repo.RenderEditProject)
|
||||
|
|
|
@ -42,7 +42,15 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
|
|||
}
|
||||
}
|
||||
if projectID > 0 {
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil {
|
||||
project, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defaultBoard, err := project.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID, defaultBoard.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ package pull
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
|
@ -43,6 +44,9 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
|
|||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR.
|
||||
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
|
||||
|
||||
// checkInvalidation checks if the line of code comment got changed by another commit.
|
||||
// If the line got changed the comment is going to be invalidated.
|
||||
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
|
||||
|
@ -293,6 +297,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
|
|||
if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
|
||||
stale = false
|
||||
} else {
|
||||
if issue.IsClosed {
|
||||
return nil, nil, ErrSubmitReviewOnClosedPR
|
||||
}
|
||||
|
||||
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
|
||||
<div id="project-board">
|
||||
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
|
||||
<div class="board {{if .CanWriteProjects}}sortable{{end}}"{{if .CanWriteProjects}} data-url="{{$.Link}}/move"{{end}}>
|
||||
{{range .Columns}}
|
||||
<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
||||
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
|
||||
|
@ -90,7 +90,7 @@
|
|||
data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
|
||||
data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
|
||||
data-url="{{$.Link}}/{{.ID}}/default">
|
||||
{{svg "octicon-pin"}}
|
||||
{{svg "octicon-star"}}
|
||||
{{ctx.Locale.Tr "repo.projects.column.set_default"}}
|
||||
</a>
|
||||
<a class="item show-modal button show-delete-project-column-modal"
|
||||
|
|
|
@ -30,20 +30,24 @@
|
|||
{{end}}
|
||||
<div class="divider"></div>
|
||||
{{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
|
||||
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
{{if not $.Issue.IsClosed}}
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
|
||||
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
|
||||
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
{{if not $.Issue.IsClosed}}
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
|
||||
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -5,12 +5,15 @@ package integration
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
@ -176,3 +179,82 @@ func TestPullView_CodeOwner(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
user1Session := loginUser(t, "user1")
|
||||
user2Session := loginUser(t, "user2")
|
||||
|
||||
// Have user1 create a fork of repo1.
|
||||
testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1")
|
||||
|
||||
t.Run("Submit approve/reject review on merged PR", func(t *testing.T) {
|
||||
// Create a merged PR (made by user1) in the upstream repo1.
|
||||
testEditFile(t, user1Session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
||||
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "master", "This is a pull title")
|
||||
elem := strings.Split(test.RedirectURL(resp), "/")
|
||||
assert.EqualValues(t, "pulls", elem[3])
|
||||
testPullMerge(t, user1Session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
|
||||
|
||||
// Grab the CSRF token.
|
||||
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Submit an approve review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
|
||||
|
||||
// Submit a reject review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
|
||||
})
|
||||
|
||||
t.Run("Submit approve/reject review on closed PR", func(t *testing.T) {
|
||||
// Created a closed PR (made by user1) in the upstream repo1.
|
||||
testEditFileToNewBranch(t, user1Session, "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n")
|
||||
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title")
|
||||
elem := strings.Split(test.RedirectURL(resp), "/")
|
||||
assert.EqualValues(t, "pulls", elem[3])
|
||||
testIssueClose(t, user1Session, elem[1], elem[2], elem[4])
|
||||
|
||||
// Grab the CSRF token.
|
||||
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Submit an approve review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
|
||||
|
||||
// Submit a reject review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder {
|
||||
options := map[string]string{
|
||||
"_csrf": csrf,
|
||||
"commit_id": "",
|
||||
"content": "test",
|
||||
"type": reviewType,
|
||||
}
|
||||
|
||||
submitURL := path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit")
|
||||
req := NewRequestWithValues(t, "POST", submitURL, options)
|
||||
return session.MakeRequest(t, req, expectedSubmitStatus)
|
||||
}
|
||||
|
||||
func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
closeURL := path.Join(owner, repo, "issues", issueNumber, "comments")
|
||||
|
||||
options := map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"status": "close",
|
||||
}
|
||||
|
||||
req = NewRequestWithValues(t, "POST", closeURL, options)
|
||||
return session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'jquery';
|
|||
import {contrastColor} from '../utils/color.js';
|
||||
import {createSortable} from '../modules/sortable.js';
|
||||
import {POST, DELETE, PUT} from '../modules/fetch.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
function updateIssueCount(cards) {
|
||||
const parent = cards.parentElement;
|
||||
|
@ -63,17 +62,20 @@ async function initRepoProjectSortable() {
|
|||
delay: 500,
|
||||
onSort: async () => {
|
||||
boardColumns = mainBoard.getElementsByClassName('project-column');
|
||||
for (let i = 0; i < boardColumns.length; i++) {
|
||||
const column = boardColumns[i];
|
||||
if (parseInt(column.getAttribute('data-sorting')) !== i) {
|
||||
try {
|
||||
const bgColor = column.style.backgroundColor; // will be rgb() string
|
||||
const color = bgColor ? tinycolor(bgColor).toHexString() : '';
|
||||
await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const columnSorting = {
|
||||
columns: Array.from(boardColumns, (column, i) => ({
|
||||
columnID: parseInt(column.getAttribute('data-id')),
|
||||
sorting: i,
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
await POST(mainBoard.getAttribute('data-url'), {
|
||||
data: columnSorting,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue