mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 10:58:11 +01:00
Add issue comment when moving issues from one column to another of the project (#29311)
Fix #27278 Replace #27816 This PR adds a meta-comment for an issue when dragging an issue from one column to another of a project. <img width="600" alt="image" src="https://github.com/go-gitea/gitea/assets/81045/5fc1d954-430e-4db0-aaee-a00006fa91f5"> --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: yp05327 <576951401@qq.com>
This commit is contained in:
parent
aa1055fe16
commit
791d7fc76a
@ -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,omitempty"`
|
||||||
|
ProjectColumnTitle string `json:"project_column_title,omitempty"`
|
||||||
|
ProjectTitle string `json:"project_title,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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.ProjectColumnTitle != "" {
|
||||||
|
commentMetaData = &CommentMetaData{
|
||||||
|
ProjectColumnID: opts.ProjectColumnID,
|
||||||
|
ProjectColumnTitle: opts.ProjectColumnTitle,
|
||||||
|
ProjectTitle: opts.ProjectTitle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -987,6 +1006,9 @@ type CreateCommentOptions struct {
|
|||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
OldProjectID int64
|
OldProjectID int64
|
||||||
ProjectID int64
|
ProjectID int64
|
||||||
|
ProjectTitle string
|
||||||
|
ProjectColumnID int64
|
||||||
|
ProjectColumnTitle string
|
||||||
TimeID int64
|
TimeID int64
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
AssigneeTeamID int64
|
AssigneeTeamID int64
|
||||||
|
@ -441,6 +441,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
|
||||||
|
@ -597,6 +597,8 @@ var migrations = []Migration{
|
|||||||
NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable),
|
NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable),
|
||||||
// v302 -> v303
|
// v302 -> v303
|
||||||
NewMigration("Add index to action_task stopped log_expired", v1_23.AddIndexToActionTaskStoppedLogExpired),
|
NewMigration("Add index to action_task stopped log_expired", v1_23.AddIndexToActionTaskStoppedLogExpired),
|
||||||
|
// v303 -> v304
|
||||||
|
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
23
models/migrations/v1_23/v303.go
Normal file
23
models/migrations/v1_23/v303.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// 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"`
|
||||||
|
ProjectColumnTitle string `json:"project_column_title"`
|
||||||
|
ProjectTitle string `json:"project_title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
@ -76,30 +76,6 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
|
|||||||
return int(c)
|
return int(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
|
|
||||||
func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
sess := db.GetEngine(ctx)
|
|
||||||
issueIDs := util.ValuesOfMap(sortedIssueIDs)
|
|
||||||
|
|
||||||
count, err := sess.Table(new(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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||||
if c.ProjectID != newColumn.ProjectID {
|
if c.ProjectID != newColumn.ProjectID {
|
||||||
return fmt.Errorf("columns have to be in the same project")
|
return fmt.Errorf("columns have to be in the same project")
|
||||||
|
@ -1476,6 +1476,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`
|
||||||
|
@ -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 (
|
||||||
@ -601,7 +602,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
|
||||||
}
|
}
|
||||||
|
@ -1687,6 +1687,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)
|
||||||
|
@ -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 (
|
||||||
@ -664,7 +665,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
|
||||||
}
|
}
|
||||||
|
79
services/projects/issue.go
Normal file
79
services/projects/issue.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// 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 {
|
||||||
|
issueIDs := make([]int64, 0, len(sortedIssueIDs))
|
||||||
|
for _, issueID := range sortedIssueIDs {
|
||||||
|
issueIDs = append(issueIDs, issueID)
|
||||||
|
}
|
||||||
|
count, err := db.GetEngine(ctx).
|
||||||
|
Where("project_id=?", column.ProjectID).
|
||||||
|
In("issue_id", issueIDs).
|
||||||
|
Count(new(project_model.ProjectIssue))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
issuesMap := make(map[int64]*issues_model.Issue, len(issues))
|
||||||
|
for _, issue := range issues {
|
||||||
|
issuesMap[issue.ID] = issue
|
||||||
|
}
|
||||||
|
|
||||||
|
for sorting, issueID := range sortedIssueIDs {
|
||||||
|
curIssue := issuesMap[issueID]
|
||||||
|
if curIssue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
@ -604,6 +604,22 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{{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}}
|
||||||
|
{{$newProjectDisplay := .CommentMetaData.ProjectTitle}}
|
||||||
|
{{if .Project}}
|
||||||
|
{{$trKey := printf "projects.type-%d.display_name" .Project.Type}}
|
||||||
|
{{$newProjectDisplay = HTMLFormat `%s <a href="%s"><span data-tooltip-content="%s">%s</span></a>` (svg .Project.IconName) (.Project.Link ctx) (ctx.Locale.Tr $trKey) .Project.Title}}
|
||||||
|
{{end}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.move_to_column_of_project" .CommentMetaData.ProjectColumnTitle $newProjectDisplay $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}}">
|
||||||
|
Loading…
Reference in New Issue
Block a user