@@ -8,7 +8,7 @@ | |||
id: 2 | |||
issue_id: 2 | |||
project_id: 1 | |||
project_board_id: 0 # no board assigned | |||
project_board_id: 5 | |||
- | |||
id: 3 |
@@ -220,6 +220,13 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { | |||
return lang.TrString("repo.issues.role." + string(r) + "_helper") | |||
} | |||
// CommentProjectBoardExtendData extend data of CommentTypeProjectBoard, | |||
// will be store in `Comment.Content` as json format | |||
type CommentProjectBoardExtendData struct { | |||
FromBoardTitle string | |||
ToBoardTitle string | |||
} | |||
// Comment represents a comment in commit and issue page. | |||
type Comment struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
@@ -301,6 +308,8 @@ type Comment struct { | |||
NewCommit string `xorm:"-"` | |||
CommitsNum int64 `xorm:"-"` | |||
IsForcePush bool `xorm:"-"` | |||
ProjectBoard *CommentProjectBoardExtendData `xorm:"-"` | |||
} | |||
func init() { | |||
@@ -539,6 +548,15 @@ func (c *Comment) LoadProject(ctx context.Context) error { | |||
return nil | |||
} | |||
func (c *Comment) LoadProjectBoard() error { | |||
if c.Type != CommentTypeProjectBoard || c.ProjectBoard != nil { | |||
return nil | |||
} | |||
c.ProjectBoard = &CommentProjectBoardExtendData{} | |||
return json.Unmarshal([]byte(c.Content), c.ProjectBoard) | |||
} | |||
// LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone | |||
func (c *Comment) LoadMilestone(ctx context.Context) error { | |||
if c.OldMilestoneID > 0 { | |||
@@ -828,6 +846,15 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, | |||
IsForcePush: opts.IsForcePush, | |||
Invalidated: opts.Invalidated, | |||
} | |||
if comment.Type == CommentTypeProjectBoard { | |||
extDataJSON, err := json.Marshal(opts.ProjectBoard) | |||
if err != nil { | |||
return nil, err | |||
} | |||
comment.Content = string(extDataJSON) | |||
comment.ProjectBoard = opts.ProjectBoard | |||
} | |||
if _, err = e.Insert(comment); err != nil { | |||
return nil, err | |||
} | |||
@@ -1007,6 +1034,8 @@ type CreateCommentOptions struct { | |||
RefIsPull bool | |||
IsForcePush bool | |||
Invalidated bool | |||
ProjectBoard *CommentProjectBoardExtendData | |||
} | |||
// GetCommentByID returns the comment by given ID. |
@@ -140,6 +140,8 @@ type Issue struct { | |||
// For view issue page. | |||
ShowRole RoleDescriptor `xorm:"-"` | |||
ProjectIssue *project_model.ProjectIssue `xorm:"-"` | |||
} | |||
var ( | |||
@@ -315,6 +317,10 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { | |||
return err | |||
} | |||
if err = issue.LoadProjectIssue(ctx); err != nil { | |||
return err | |||
} | |||
if err = issue.LoadAssignees(ctx); err != nil { | |||
return err | |||
} |
@@ -226,14 +226,15 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { | |||
func (issues IssueList) LoadProjects(ctx context.Context) error { | |||
issueIDs := issues.getIssueIDs() | |||
projectMaps := make(map[int64]*project_model.Project, len(issues)) | |||
left := len(issueIDs) | |||
type projectWithIssueID struct { | |||
*project_model.Project `xorm:"extends"` | |||
IssueID int64 | |||
ProjectIssue *project_model.ProjectIssue `xorm:"extends"` | |||
} | |||
projectMaps := make(map[int64]*projectWithIssueID, len(issues)) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
@@ -243,7 +244,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { | |||
projects := make([]*projectWithIssueID, 0, limit) | |||
err := db.GetEngine(ctx). | |||
Table("project"). | |||
Select("project.*, project_issue.issue_id"). | |||
Select("project.*, project_issue.*"). | |||
Join("INNER", "project_issue", "project.id = project_issue.project_id"). | |||
In("project_issue.issue_id", issueIDs[:limit]). | |||
Find(&projects) | |||
@@ -251,14 +252,20 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { | |||
return err | |||
} | |||
for _, project := range projects { | |||
projectMaps[project.IssueID] = project.Project | |||
projectMaps[project.ProjectIssue.IssueID] = project | |||
} | |||
left -= limit | |||
issueIDs = issueIDs[limit:] | |||
} | |||
for _, issue := range issues { | |||
issue.Project = projectMaps[issue.ID] | |||
item, exist := projectMaps[issue.ID] | |||
if !exist { | |||
continue | |||
} | |||
issue.Project = item.Project | |||
issue.ProjectIssue = item.ProjectIssue | |||
} | |||
return nil | |||
} | |||
@@ -554,6 +561,10 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error { | |||
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err) | |||
} | |||
if err := issues.LoadProjectIssueBoards(ctx); err != nil { | |||
return fmt.Errorf("issue.loadAttributes: LoadProjectIssueBoards: %w", err) | |||
} | |||
if err := issues.loadAssignees(ctx); err != nil { | |||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err) | |||
} | |||
@@ -626,3 +637,60 @@ func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error { | |||
return nil | |||
} | |||
func (issues IssueList) getProjectIssueBoardIDs() []int64 { | |||
boardIDmap := make(map[int64]bool, 5) | |||
for _, issue := range issues { | |||
if issue.ProjectIssue != nil { | |||
boardIDmap[issue.ProjectIssue.ProjectBoardID] = true | |||
} | |||
} | |||
bordIDs := make([]int64, 0, len(boardIDmap)) | |||
for id := range boardIDmap { | |||
bordIDs = append(bordIDs, id) | |||
} | |||
return bordIDs | |||
} | |||
func (issues IssueList) LoadProjectIssueBoards(ctx context.Context) error { | |||
boardIDs := issues.getProjectIssueBoardIDs() | |||
if len(boardIDs) == 0 { | |||
return nil | |||
} | |||
boardMaps := make(map[int64]*project_model.Board, len(boardIDs)) | |||
left := len(boardIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
err := db.GetEngine(ctx). | |||
In("id", boardIDs[:limit]). | |||
Find(&boardMaps) | |||
if err != nil { | |||
return err | |||
} | |||
left -= limit | |||
boardIDs = boardIDs[limit:] | |||
} | |||
for _, issue := range issues { | |||
if issue.ProjectIssue != nil { | |||
board, exist := boardMaps[issue.ProjectIssue.ProjectBoardID] | |||
if exist { | |||
issue.ProjectIssue.ProjectBoard = board | |||
} else { | |||
issue.ProjectIssue.ProjectBoard = &project_model.Board{ | |||
ID: -1, | |||
Title: "Deleted", | |||
} | |||
} | |||
} | |||
} | |||
return nil | |||
} |
@@ -68,6 +68,10 @@ func TestIssueList_LoadAttributes(t *testing.T) { | |||
assert.Equal(t, int64(400), issue.TotalTrackedTime) | |||
assert.NotNil(t, issue.Project) | |||
assert.Equal(t, int64(1), issue.Project.ID) | |||
assert.NotNil(t, issue.ProjectIssue) | |||
assert.Equal(t, int64(1), issue.ProjectIssue.IssueID) | |||
assert.NotNil(t, issue.ProjectIssue.ProjectBoard) | |||
assert.Equal(t, int64(1), issue.ProjectIssue.ProjectBoard.ID) | |||
} else { | |||
assert.Nil(t, issue.Project) | |||
} |
@@ -28,6 +28,23 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) { | |||
return err | |||
} | |||
func (issue *Issue) LoadProjectIssue(ctx context.Context) (err error) { | |||
if issue.Project == nil { | |||
return nil | |||
} | |||
if issue.ProjectIssue != nil { | |||
return nil | |||
} | |||
issue.ProjectIssue, err = project_model.GetProjectIssueByIssueID(ctx, issue.ID) | |||
if err != nil { | |||
return err | |||
} | |||
return issue.ProjectIssue.LoadProjectBoard(ctx) | |||
} | |||
func (issue *Issue) projectID(ctx context.Context) int64 { | |||
var ip project_model.ProjectIssue | |||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) | |||
@@ -107,6 +124,7 @@ func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.Use | |||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { | |||
oldProjectID := issue.projectID(ctx) | |||
newBoardID := int64(0) | |||
if err := issue.LoadRepo(ctx); err != nil { | |||
return err | |||
@@ -121,6 +139,12 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U | |||
if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID { | |||
return fmt.Errorf("issue's repository is not the same as project's repository") | |||
} | |||
newBoard, err := newProject.GetDefaultBoard(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
newBoardID = newBoard.ID | |||
} | |||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { | |||
@@ -141,7 +165,8 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U | |||
} | |||
return db.Insert(ctx, &project_model.ProjectIssue{ | |||
IssueID: issue.ID, | |||
ProjectID: newProjectID, | |||
IssueID: issue.ID, | |||
ProjectID: newProjectID, | |||
ProjectBoardID: newBoardID, | |||
}) | |||
} |
@@ -0,0 +1,95 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package issues | |||
import ( | |||
"context" | |||
"errors" | |||
"sort" | |||
"code.gitea.io/gitea/models/db" | |||
project_model "code.gitea.io/gitea/models/project" | |||
user_model "code.gitea.io/gitea/models/user" | |||
) | |||
type ProjectMovedIssuesFormItem struct { | |||
IssueID int64 `json:"issueID"` | |||
Sorting int64 `json:"sorting"` | |||
} | |||
type ProjectMovedIssuesForm struct { | |||
Issues []ProjectMovedIssuesFormItem `json:"issues"` | |||
} | |||
func (p *ProjectMovedIssuesForm) ToSortedIssueIDs() (issueIDs, issueSorts []int64) { | |||
sort.Slice(p.Issues, func(i, j int) bool { return p.Issues[i].Sorting < p.Issues[j].Sorting }) | |||
issueIDs = make([]int64, 0, len(p.Issues)) | |||
issueSorts = make([]int64, 0, len(p.Issues)) | |||
for _, issue := range p.Issues { | |||
issueIDs = append(issueIDs, issue.IssueID) | |||
issueSorts = append(issueSorts, issue.Sorting) | |||
} | |||
return issueIDs, issueSorts | |||
} | |||
func MoveIssuesOnProjectBoard(ctx context.Context, doer *user_model.User, form *ProjectMovedIssuesForm, project *project_model.Project, board *project_model.Board) error { | |||
issueIDs, issueSorts := form.ToSortedIssueIDs() | |||
movedIssues, err := GetIssuesByIDs(ctx, issueIDs) | |||
if err != nil { | |||
return err | |||
} | |||
if len(movedIssues) != len(form.Issues) { | |||
return errors.New("some issues do not exist") | |||
} | |||
if _, err = movedIssues.LoadRepositories(ctx); err != nil { | |||
return err | |||
} | |||
if err = movedIssues.LoadProjects(ctx); err != nil { | |||
return err | |||
} | |||
if err = movedIssues.LoadProjectIssueBoards(ctx); err != nil { | |||
return err | |||
} | |||
for _, issue := range movedIssues { | |||
if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID { | |||
return errors.New("Some issue's repoID is not equal to project's repoID") | |||
} | |||
} | |||
return db.WithTx(ctx, func(ctx context.Context) error { | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, issueIDs, issueSorts); err != nil { | |||
return err | |||
} | |||
for _, issue := range movedIssues { | |||
if issue.ProjectIssue.ProjectBoardID == board.ID { | |||
continue | |||
} | |||
_, err = CreateComment(ctx, &CreateCommentOptions{ | |||
Type: CommentTypeProjectBoard, | |||
Doer: doer, | |||
Repo: issue.Repo, | |||
Issue: issue, | |||
ProjectID: project.ID, | |||
ProjectBoard: &CommentProjectBoardExtendData{ | |||
FromBoardTitle: issue.ProjectIssue.ProjectBoard.Title, | |||
ToBoardTitle: board.Title, | |||
}, | |||
}) | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
}) | |||
} |
@@ -0,0 +1,78 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package issues | |||
import ( | |||
"testing" | |||
"code.gitea.io/gitea/models/db" | |||
project_model "code.gitea.io/gitea/models/project" | |||
"code.gitea.io/gitea/models/unittest" | |||
user_model "code.gitea.io/gitea/models/user" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestProjectMovedIssuesForm_ToSortedIssueIDs(t *testing.T) { | |||
opts := &ProjectMovedIssuesForm{ | |||
Issues: []ProjectMovedIssuesFormItem{ | |||
{ | |||
IssueID: 5, | |||
Sorting: 1, | |||
}, | |||
{ | |||
IssueID: 1, | |||
Sorting: 4, | |||
}, | |||
{ | |||
IssueID: 6, | |||
Sorting: 3, | |||
}, | |||
}, | |||
} | |||
ids, sorts := opts.ToSortedIssueIDs() | |||
assert.EqualValues(t, sorts, []int64{1, 3, 4}) | |||
assert.EqualValues(t, ids, []int64{5, 6, 1}) | |||
} | |||
func TestMoveIssuesOnProjectBoard(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | |||
project := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1}) | |||
toBoard := unittest.AssertExistsAndLoadBean(t, &project_model.Board{ID: 2}) | |||
list, err := LoadIssuesFromBoardList(db.DefaultContext, []*project_model.Board{toBoard}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 1, len(list[toBoard.ID])) | |||
assert.EqualValues(t, 3, list[toBoard.ID][0].ID) | |||
opts := &ProjectMovedIssuesForm{ | |||
Issues: []ProjectMovedIssuesFormItem{ | |||
{ | |||
IssueID: 1, | |||
Sorting: 2, | |||
}, | |||
{ | |||
IssueID: 2, | |||
Sorting: 3, | |||
}, | |||
{ | |||
IssueID: 3, | |||
Sorting: 1, | |||
}, | |||
}, | |||
} | |||
assert.NoError(t, MoveIssuesOnProjectBoard(db.DefaultContext, doer, opts, project, toBoard)) | |||
list, err = LoadIssuesFromBoardList(db.DefaultContext, []*project_model.Board{toBoard}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 3, len(list[toBoard.ID])) | |||
assert.EqualValues(t, 3, list[toBoard.ID][0].ID) | |||
assert.EqualValues(t, 1, list[toBoard.ID][1].ID) | |||
assert.EqualValues(t, 2, list[toBoard.ID][2].ID) | |||
} |
@@ -247,7 +247,7 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { | |||
return nil, err | |||
} | |||
defaultB, err := p.getDefaultBoard(ctx) | |||
defaultB, err := p.GetDefaultBoard(ctx) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -255,8 +255,8 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { | |||
return append([]*Board{defaultB}, 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). |
@@ -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,8 +17,8 @@ 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 | |||
ProjectBoardID int64 `xorm:"INDEX"` | |||
ProjectBoardID int64 `xorm:"INDEX"` | |||
ProjectBoard *Board `xorm:"-"` | |||
// the sorting order on the board | |||
Sorting int64 `xorm:"NOT NULL DEFAULT 0"` | |||
@@ -76,33 +76,76 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { | |||
} | |||
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column | |||
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { | |||
return db.WithTx(ctx, func(ctx context.Context) error { | |||
sess := db.GetEngine(ctx) | |||
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, issueIDs, issueSorts []int64) 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() | |||
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() | |||
if err != nil { | |||
return err | |||
} | |||
if int(count) != len(issueIDs) { | |||
return fmt.Errorf("all issues have to be added to a project first") | |||
} | |||
for i, issueID := range issueIDs { | |||
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, issueSorts[i], issueID) | |||
if err != nil { | |||
return err | |||
} | |||
if int(count) != len(sortedIssueIDs) { | |||
return fmt.Errorf("all issues have to be added to a project first") | |||
} | |||
} | |||
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 | |||
}) | |||
return nil | |||
} | |||
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) | |||
return err | |||
} | |||
type ErrProjectIssueNotExist struct { | |||
IssueID int64 | |||
} | |||
func (e ErrProjectIssueNotExist) Error() string { | |||
return fmt.Sprintf("can't find project issue [issue_id: %d]", e.IssueID) | |||
} | |||
func IsErrProjectIssueNotExist(e error) bool { | |||
_, ok := e.(ErrProjectIssueNotExist) | |||
return ok | |||
} | |||
func GetProjectIssueByIssueID(ctx context.Context, issueID int64) (*ProjectIssue, error) { | |||
issue := &ProjectIssue{} | |||
has, err := db.GetEngine(ctx).Where("issue_id = ?", issueID).Get(issue) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if !has { | |||
return nil, ErrProjectIssueNotExist{IssueID: issueID} | |||
} | |||
return issue, nil | |||
} | |||
func (issue *ProjectIssue) LoadProjectBoard(ctx context.Context) error { | |||
if issue.ProjectBoard != nil { | |||
return nil | |||
} | |||
var err error | |||
issue.ProjectBoard, err = GetBoard(ctx, issue.ProjectBoardID) | |||
if IsErrProjectBoardNotExist(err) { | |||
issue.ProjectBoard = &Board{ | |||
ID: -1, | |||
Title: "Deleted", | |||
} | |||
return nil | |||
} | |||
return err | |||
} |
@@ -373,12 +373,6 @@ func searchIssueInProject(t *testing.T) { | |||
}, | |||
[]int64{1}, | |||
}, | |||
{ | |||
SearchOptions{ | |||
ProjectBoardID: optional.Some(int64(0)), // issue with in default board | |||
}, | |||
[]int64{2}, | |||
}, | |||
} | |||
for _, test := range tests { | |||
issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) |
@@ -1471,6 +1471,7 @@ issues.add_milestone_at = `added this to the <b>%s</b> milestone %s` | |||
issues.add_project_at = `added this to the <b>%s</b> project %s` | |||
issues.change_milestone_at = `modified the milestone from <b>%s</b> to <b>%s</b> %s` | |||
issues.change_project_at = `modified the project from <b>%s</b> to <b>%s</b> %s` | |||
issues.change_project_board_at = `moved this from <b>%s</b> to <b>%s</b> in <b>%s</b> %s` | |||
issues.remove_milestone_at = `removed this from the <b>%s</b> milestone %s` | |||
issues.remove_project_at = `removed this from the <b>%s</b> project %s` | |||
issues.deleted_milestone = `(deleted)` |
@@ -630,48 +630,14 @@ func MoveIssues(ctx *context.Context) { | |||
return | |||
} | |||
type movedIssuesForm struct { | |||
Issues []struct { | |||
IssueID int64 `json:"issueID"` | |||
Sorting int64 `json:"sorting"` | |||
} `json:"issues"` | |||
} | |||
form := &movedIssuesForm{} | |||
form := &issues_model.ProjectMovedIssuesForm{} | |||
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 := issues_model.GetIssuesByIDs(ctx, issueIDs) | |||
if err != nil { | |||
ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err) | |||
return | |||
} | |||
if len(movedIssues) != len(form.Issues) { | |||
ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) | |||
return | |||
} | |||
if _, err = movedIssues.LoadRepositories(ctx); err != nil { | |||
ctx.ServerError("LoadRepositories", err) | |||
return | |||
} | |||
for _, issue := range movedIssues { | |||
if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID { | |||
ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID")) | |||
return | |||
} | |||
} | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { | |||
err = issues_model.MoveIssuesOnProjectBoard(ctx, ctx.Doer, form, project, board) | |||
if err != nil { | |||
ctx.ServerError("MoveIssuesOnProjectBoard", err) | |||
return | |||
} |
@@ -1662,11 +1662,15 @@ func ViewIssue(ctx *context.Context) { | |||
if comment.MilestoneID > 0 && comment.Milestone == nil { | |||
comment.Milestone = ghostMilestone | |||
} | |||
} else if comment.Type == issues_model.CommentTypeProject { | |||
} else if comment.Type == issues_model.CommentTypeProject || comment.Type == issues_model.CommentTypeProjectBoard { | |||
if err = comment.LoadProject(ctx); err != nil { | |||
ctx.ServerError("LoadProject", err) | |||
return | |||
} | |||
if err = comment.LoadProjectBoard(); err != nil { | |||
ctx.ServerError("LoadProjectBoard", err) | |||
return | |||
} | |||
ghostProject := &project_model.Project{ | |||
ID: -1, |
@@ -4,7 +4,6 @@ | |||
package repo | |||
import ( | |||
"errors" | |||
"fmt" | |||
"net/http" | |||
"strings" | |||
@@ -619,47 +618,14 @@ func MoveIssues(ctx *context.Context) { | |||
return | |||
} | |||
type movedIssuesForm struct { | |||
Issues []struct { | |||
IssueID int64 `json:"issueID"` | |||
Sorting int64 `json:"sorting"` | |||
} `json:"issues"` | |||
} | |||
form := &movedIssuesForm{} | |||
form := &issues_model.ProjectMovedIssuesForm{} | |||
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 := issues_model.GetIssuesByIDs(ctx, issueIDs) | |||
if err != nil { | |||
if issues_model.IsErrIssueNotExist(err) { | |||
ctx.NotFound("IssueNotExisting", nil) | |||
} else { | |||
ctx.ServerError("GetIssueByID", err) | |||
} | |||
return | |||
} | |||
if len(movedIssues) != len(form.Issues) { | |||
ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) | |||
return | |||
} | |||
for _, issue := range movedIssues { | |||
if issue.RepoID != project.RepoID { | |||
ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID")) | |||
return | |||
} | |||
} | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { | |||
err = issues_model.MoveIssuesOnProjectBoard(ctx, ctx.Doer, form, project, board) | |||
if err != nil { | |||
ctx.ServerError("MoveIssuesOnProjectBoard", err) | |||
return | |||
} |
@@ -600,6 +600,26 @@ | |||
</span> | |||
</div> | |||
{{end}} | |||
{{else if eq .Type 31}} | |||
{{if not $.UnitProjectsGlobalDisabled}} | |||
<div class="timeline-item event" id="{{.HashTag}}"> | |||
<span class="badge">{{svg "octicon-project"}}</span> | |||
{{template "shared/user/avatarlink" dict "user" .Poster}} | |||
<span class="text grey muted-links"> | |||
{{template "shared/user/authorlink" .Poster}} | |||
{{$projectDisplayHtml := "Unknown Project"}} | |||
{{if .Project}} | |||
{{$trKey := printf "projects.type-%d.display_name" .Project.Type}} | |||
{{$projectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .Project.Title}} | |||
{{end}} | |||
{{if gt .ProjectID 0}} | |||
{{ctx.Locale.Tr "repo.issues.change_project_board_at" .ProjectBoard.FromBoardTitle .ProjectBoard.ToBoardTitle $projectDisplayHtml $createdStr}} | |||
{{end}} | |||
</span> | |||
</div> | |||
{{end}} | |||
{{else if eq .Type 32}} | |||
<div class="timeline-item-group"> | |||
<div class="timeline-item event" id="{{.HashTag}}"> |