1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-09-28 03:06:03 -04:00

Add issue comment when moving issues from one column to another of the project

This commit is contained in:
Lunny Xiao 2024-02-22 12:17:38 +08:00
parent cd7d1314fc
commit 5f5212ca60
11 changed files with 177 additions and 31 deletions

View File

@ -222,6 +222,13 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
return lang.TrString("repo.issues.role." + string(r) + "_helper") return lang.TrString("repo.issues.role." + string(r) + "_helper")
} }
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
type CommentMetaData struct {
ProjectColumnID int64 `json:"project_column_id"`
ProjectColumnTitle string `json:"project_column_name"`
ProjectTitle string `json:"project_name"`
}
// Comment represents a comment in commit and issue page. // Comment represents a comment in commit and issue page.
type Comment struct { type Comment struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -295,6 +302,8 @@ type Comment struct {
RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves
RefIsPull bool RefIsPull bool
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
RefRepo *repo_model.Repository `xorm:"-"` RefRepo *repo_model.Repository `xorm:"-"`
RefIssue *Issue `xorm:"-"` RefIssue *Issue `xorm:"-"`
RefComment *Comment `xorm:"-"` RefComment *Comment `xorm:"-"`
@ -797,6 +806,15 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
LabelID = opts.Label.ID LabelID = opts.Label.ID
} }
var commentMetaData *CommentMetaData
if opts.ProjectColumnID > 0 {
commentMetaData = &CommentMetaData{
ProjectColumnID: opts.ProjectColumnID,
ProjectColumnTitle: opts.ProjectColumnTitle,
ProjectTitle: opts.ProjectColumnTitle,
}
}
comment := &Comment{ comment := &Comment{
Type: opts.Type, Type: opts.Type,
PosterID: opts.Doer.ID, PosterID: opts.Doer.ID,
@ -830,6 +848,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
RefIsPull: opts.RefIsPull, RefIsPull: opts.RefIsPull,
IsForcePush: opts.IsForcePush, IsForcePush: opts.IsForcePush,
Invalidated: opts.Invalidated, Invalidated: opts.Invalidated,
CommentMetaData: commentMetaData,
} }
if _, err = e.Insert(comment); err != nil { if _, err = e.Insert(comment); err != nil {
return nil, err return nil, err
@ -982,34 +1001,37 @@ type CreateCommentOptions struct {
Issue *Issue Issue *Issue
Label *Label Label *Label
DependentIssueID int64 DependentIssueID int64
OldMilestoneID int64 OldMilestoneID int64
MilestoneID int64 MilestoneID int64
OldProjectID int64 OldProjectID int64
ProjectID int64 ProjectID int64
TimeID int64 ProjectTitle string
AssigneeID int64 ProjectColumnID int64
AssigneeTeamID int64 ProjectColumnTitle string
RemovedAssignee bool TimeID int64
OldTitle string AssigneeID int64
NewTitle string AssigneeTeamID int64
OldRef string RemovedAssignee bool
NewRef string OldTitle string
CommitID int64 NewTitle string
CommitSHA string OldRef string
Patch string NewRef string
LineNum int64 CommitID int64
TreePath string CommitSHA string
ReviewID int64 Patch string
Content string LineNum int64
Attachments []string // UUIDs of attachments TreePath string
RefRepoID int64 ReviewID int64
RefIssueID int64 Content string
RefCommentID int64 Attachments []string // UUIDs of attachments
RefAction references.XRefAction RefRepoID int64
RefIsPull bool RefIssueID int64
IsForcePush bool RefCommentID int64
Invalidated bool RefAction references.XRefAction
RefIsPull bool
IsForcePush bool
Invalidated bool
} }
// GetCommentByID returns the comment by given ID. // GetCommentByID returns the comment by given ID.

View File

@ -434,6 +434,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er
Join("INNER", "issue", "issue.id = comment.issue_id"). Join("INNER", "issue", "issue.id = comment.issue_id").
In("issue.id", issuesIDs[:limit]). In("issue.id", issuesIDs[:limit]).
Where(cond). Where(cond).
NoAutoCondition().
Rows(new(Comment)) Rows(new(Comment))
if err != nil { if err != nil {
return err return err

View File

@ -591,6 +591,8 @@ var migrations = []Migration{
// v299 -> v300 // v299 -> v300
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment), NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
// v300 -> v301
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View File

@ -1,6 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package v1_22 //nolint package v1_22 //nolint
import ( import (

View File

@ -0,0 +1,22 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"xorm.io/xorm"
)
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
type CommentMetaData struct {
ProjectColumnID int64 `json:"project_column_id"`
ProjectColumnName string `json:"project_column_name"`
ProjectName string `json:"project_name"`
}
func AddCommentMetaDataColumn(x *xorm.Engine) error {
type Comment struct {
CommentMetaData CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
}
return x.Sync(new(Comment))
}

View File

@ -1470,6 +1470,7 @@ issues.remove_labels = removed the %s labels %s
issues.add_remove_labels = added %s and removed %s labels %s issues.add_remove_labels = added %s and removed %s labels %s
issues.add_milestone_at = `added this to the <b>%s</b> milestone %s` 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.add_project_at = `added this to the <b>%s</b> project %s`
issues.move_to_column_of_project = `moved this to %s in %s on %s`
issues.change_milestone_at = `modified the milestone from <b>%s</b> to <b>%s</b> %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_at = `modified the project from <b>%s</b> to <b>%s</b> %s`
issues.remove_milestone_at = `removed this from the <b>%s</b> milestone %s` issues.remove_milestone_at = `removed this from the <b>%s</b> milestone %s`

View File

@ -23,6 +23,7 @@ import (
shared_user "code.gitea.io/gitea/routers/web/shared/user" shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
project_service "code.gitea.io/gitea/services/projects"
) )
const ( const (
@ -600,7 +601,7 @@ func MoveIssues(ctx *context.Context) {
} }
} }
if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil { if err = project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectColumn", err) ctx.ServerError("MoveIssuesOnProjectColumn", err)
return return
} }

View File

@ -1680,6 +1680,11 @@ func ViewIssue(ctx *context.Context) {
if comment.ProjectID > 0 && comment.Project == nil { if comment.ProjectID > 0 && comment.Project == nil {
comment.Project = ghostProject comment.Project = ghostProject
} }
} else if comment.Type == issues_model.CommentTypeProjectColumn {
if err = comment.LoadProject(ctx); err != nil {
ctx.ServerError("LoadProject", err)
return
}
} else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest { } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil { if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil {
ctx.ServerError("LoadAssigneeUserAndTeam", err) ctx.ServerError("LoadAssigneeUserAndTeam", err)

View File

@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
project_service "code.gitea.io/gitea/services/projects"
) )
const ( const (
@ -662,7 +663,7 @@ func MoveIssues(ctx *context.Context) {
} }
} }
if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil { if err = project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectColumn", err) ctx.ServerError("MoveIssuesOnProjectColumn", err)
return return
} }

View File

@ -0,0 +1,76 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
user_model "code.gitea.io/gitea/models/user"
)
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, column *project_model.Column, sortedIssueIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)
issueIDs := make([]int64, 0, len(sortedIssueIDs))
for _, issueID := range sortedIssueIDs {
issueIDs = append(issueIDs, issueID)
}
count, err := sess.Table(new(project_model.ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count()
if err != nil {
return err
}
if int(count) != len(sortedIssueIDs) {
return fmt.Errorf("all issues have to be added to a project first")
}
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
return err
}
if _, err := issues.LoadRepositories(ctx); err != nil {
return err
}
project, err := project_model.GetProjectByID(ctx, column.ProjectID)
if err != nil {
return err
}
for sorting, issueID := range sortedIssueIDs {
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
if err != nil {
return err
}
var curIssue *issues_model.Issue
for _, issue := range issues {
if issue.ID == issueID {
curIssue = issue
break
}
}
// add timeline to issue
if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeProjectColumn,
Doer: doer,
Repo: curIssue.Repo,
Issue: curIssue,
ProjectID: column.ProjectID,
ProjectTitle: project.Title,
ProjectColumnID: column.ID,
ProjectColumnTitle: column.Title,
}); err != nil {
return err
}
}
return nil
})
}

View File

@ -600,6 +600,22 @@
</span> </span>
</div> </div>
{{end}} {{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}}
{{$newProjectDisplayHtml := "Unknown Project"}}
{{if .Project}}
{{$trKey := printf "projects.type-%d.display_name" .Project.Type}}
{{$newProjectDisplayHtml = printf `%s <a href="%s"><span data-tooltip-content="%s">%s</span></a>` (svg .Project.IconName) (.Project.Link ctx) (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}}
{{end}}
{{ctx.Locale.Tr "repo.issues.move_to_column_of_project" (.CommentMetaData.ProjectColumnTitle|Safe) ($newProjectDisplayHtml|Safe) $createdStr}}
</span>
</div>
{{end}}
{{else if eq .Type 32}} {{else if eq .Type 32}}
<div class="timeline-item-group"> <div class="timeline-item-group">
<div class="timeline-item event" id="{{.HashTag}}"> <div class="timeline-item event" id="{{.HashTag}}">