This commit is contained in:
a1012112796 2024-04-26 18:29:18 +02:00 committed by GitHub
commit 19c496655d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 414 additions and 115 deletions

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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,
})
}

95
models/issues/project.go Normal file
View File

@ -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
})
}

View File

@ -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)
}

View File

@ -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).

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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)`

View File

@ -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)
return
}
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)
err = issues_model.MoveIssuesOnProjectBoard(ctx, ctx.Doer, form, project, board)
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 {
ctx.ServerError("MoveIssuesOnProjectBoard", err)
return
}

View File

@ -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,

View File

@ -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)
return
}
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)
err = issues_model.MoveIssuesOnProjectBoard(ctx, ctx.Doer, form, project, board)
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 {
ctx.ServerError("MoveIssuesOnProjectBoard", err)
return
}

View File

@ -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}}">