diff --git a/models/fixtures/project_issue.yml b/models/fixtures/project_issue.yml index b1af05908aafb..54e288af2eccf 100644 --- a/models/fixtures/project_issue.yml +++ b/models/fixtures/project_issue.yml @@ -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 diff --git a/models/issues/comment.go b/models/issues/comment.go index 353163ebd6f99..b9ec2ee45ce67 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -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. diff --git a/models/issues/issue.go b/models/issues/issue.go index 87c1c86eb15be..74c5be1db3306 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -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 } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index f8ee271a6bbf5..ce8a494b65c10 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -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 +} diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 9069e1012da53..b812e4ff4bef6 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -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) } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 907a5a17b9f20..f0ca0fc0f0e3c 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -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, }) } diff --git a/models/issues/project.go b/models/issues/project.go new file mode 100644 index 0000000000000..060daa283f946 --- /dev/null +++ b/models/issues/project.go @@ -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 + }) +} diff --git a/models/issues/project_test.go b/models/issues/project_test.go new file mode 100644 index 0000000000000..26bfc45780a9c --- /dev/null +++ b/models/issues/project_test.go @@ -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) +} diff --git a/models/project/board.go b/models/project/board.go index 7faabc52c58bf..ef0fd13a4d49d 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -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). diff --git a/models/project/board_test.go b/models/project/board_test.go index 71ba29a5896dc..8d954d89a3ee4 100644 --- a/models/project/board_test.go +++ b/models/project/board_test.go @@ -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) diff --git a/models/project/issue.go b/models/project/issue.go index ebc9719de55d0..f1addff71fb7f 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -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 +} diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 0d0cfc851697d..b3c47f3f63391 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -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) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4f17b1a6db3ae..6637ddf124474 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1470,6 +1470,7 @@ issues.add_milestone_at = `added this to the %s milestone %s` issues.add_project_at = `added this to the %s project %s` issues.change_milestone_at = `modified the milestone from %s to %s %s` issues.change_project_at = `modified the project from %s to %s %s` +issues.change_project_board_at = `moved this from %s to %s in %s %s` issues.remove_milestone_at = `removed this from the %s milestone %s` issues.remove_project_at = `removed this from the %s project %s` issues.deleted_milestone = `(deleted)` diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 7f78d1c830b7f..cb89a2fff72cc 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -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 } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 95f0cf3d71cff..cb8a181f21efe 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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, diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 9b765e89e877f..cd2d405b5df52 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -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 } diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index acc04e4c61514..15fae0910de00 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -600,6 +600,26 @@ {{end}} + {{else if eq .Type 31}} + {{if not $.UnitProjectsGlobalDisabled}} +
+ {{svg "octicon-project"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + + {{$projectDisplayHtml := "Unknown Project"}} + {{if .Project}} + {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} + {{$projectDisplayHtml = HTMLFormat `%s` (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}} + +
+ {{end}} {{else if eq .Type 32}}