mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09: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:
		| @@ -222,6 +222,13 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { | ||||
| 	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. | ||||
| type Comment struct { | ||||
| 	ID               int64            `xorm:"pk autoincr"` | ||||
| @@ -295,6 +302,8 @@ type Comment struct { | ||||
| 	RefAction    references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves | ||||
| 	RefIsPull    bool | ||||
|  | ||||
| 	CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field | ||||
|  | ||||
| 	RefRepo    *repo_model.Repository `xorm:"-"` | ||||
| 	RefIssue   *Issue                 `xorm:"-"` | ||||
| 	RefComment *Comment               `xorm:"-"` | ||||
| @@ -797,6 +806,15 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, | ||||
| 		LabelID = opts.Label.ID | ||||
| 	} | ||||
|  | ||||
| 	var commentMetaData *CommentMetaData | ||||
| 	if opts.ProjectColumnTitle != "" { | ||||
| 		commentMetaData = &CommentMetaData{ | ||||
| 			ProjectColumnID:    opts.ProjectColumnID, | ||||
| 			ProjectColumnTitle: opts.ProjectColumnTitle, | ||||
| 			ProjectTitle:       opts.ProjectTitle, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	comment := &Comment{ | ||||
| 		Type:             opts.Type, | ||||
| 		PosterID:         opts.Doer.ID, | ||||
| @@ -830,6 +848,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, | ||||
| 		RefIsPull:        opts.RefIsPull, | ||||
| 		IsForcePush:      opts.IsForcePush, | ||||
| 		Invalidated:      opts.Invalidated, | ||||
| 		CommentMetaData:  commentMetaData, | ||||
| 	} | ||||
| 	if _, err = e.Insert(comment); err != nil { | ||||
| 		return nil, err | ||||
| @@ -982,34 +1001,37 @@ type CreateCommentOptions struct { | ||||
| 	Issue *Issue | ||||
| 	Label *Label | ||||
|  | ||||
| 	DependentIssueID int64 | ||||
| 	OldMilestoneID   int64 | ||||
| 	MilestoneID      int64 | ||||
| 	OldProjectID     int64 | ||||
| 	ProjectID        int64 | ||||
| 	TimeID           int64 | ||||
| 	AssigneeID       int64 | ||||
| 	AssigneeTeamID   int64 | ||||
| 	RemovedAssignee  bool | ||||
| 	OldTitle         string | ||||
| 	NewTitle         string | ||||
| 	OldRef           string | ||||
| 	NewRef           string | ||||
| 	CommitID         int64 | ||||
| 	CommitSHA        string | ||||
| 	Patch            string | ||||
| 	LineNum          int64 | ||||
| 	TreePath         string | ||||
| 	ReviewID         int64 | ||||
| 	Content          string | ||||
| 	Attachments      []string // UUIDs of attachments | ||||
| 	RefRepoID        int64 | ||||
| 	RefIssueID       int64 | ||||
| 	RefCommentID     int64 | ||||
| 	RefAction        references.XRefAction | ||||
| 	RefIsPull        bool | ||||
| 	IsForcePush      bool | ||||
| 	Invalidated      bool | ||||
| 	DependentIssueID   int64 | ||||
| 	OldMilestoneID     int64 | ||||
| 	MilestoneID        int64 | ||||
| 	OldProjectID       int64 | ||||
| 	ProjectID          int64 | ||||
| 	ProjectTitle       string | ||||
| 	ProjectColumnID    int64 | ||||
| 	ProjectColumnTitle string | ||||
| 	TimeID             int64 | ||||
| 	AssigneeID         int64 | ||||
| 	AssigneeTeamID     int64 | ||||
| 	RemovedAssignee    bool | ||||
| 	OldTitle           string | ||||
| 	NewTitle           string | ||||
| 	OldRef             string | ||||
| 	NewRef             string | ||||
| 	CommitID           int64 | ||||
| 	CommitSHA          string | ||||
| 	Patch              string | ||||
| 	LineNum            int64 | ||||
| 	TreePath           string | ||||
| 	ReviewID           int64 | ||||
| 	Content            string | ||||
| 	Attachments        []string // UUIDs of attachments | ||||
| 	RefRepoID          int64 | ||||
| 	RefIssueID         int64 | ||||
| 	RefCommentID       int64 | ||||
| 	RefAction          references.XRefAction | ||||
| 	RefIsPull          bool | ||||
| 	IsForcePush        bool | ||||
| 	Invalidated        bool | ||||
| } | ||||
|  | ||||
| // GetCommentByID returns the comment by given ID. | ||||
|   | ||||
| @@ -441,6 +441,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er | ||||
| 			Join("INNER", "issue", "issue.id = comment.issue_id"). | ||||
| 			In("issue.id", issuesIDs[:limit]). | ||||
| 			Where(cond). | ||||
| 			NoAutoCondition(). | ||||
| 			Rows(new(Comment)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
|   | ||||
| @@ -597,6 +597,8 @@ var migrations = []Migration{ | ||||
| 	NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable), | ||||
| 	// v302 -> v303 | ||||
| 	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 | ||||
|   | ||||
							
								
								
									
										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) | ||||
| } | ||||
|  | ||||
| // 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 { | ||||
| 	if c.ProjectID != newColumn.ProjectID { | ||||
| 		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_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.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_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` | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
| 	shared_user "code.gitea.io/gitea/routers/web/shared/user" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	project_service "code.gitea.io/gitea/services/projects" | ||||
| ) | ||||
|  | ||||
| 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) | ||||
| 		return | ||||
| 	} | ||||
|   | ||||
| @@ -1687,6 +1687,11 @@ func ViewIssue(ctx *context.Context) { | ||||
| 			if comment.ProjectID > 0 && comment.Project == nil { | ||||
| 				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 { | ||||
| 			if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil { | ||||
| 				ctx.ServerError("LoadAssigneeUserAndTeam", err) | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	project_service "code.gitea.io/gitea/services/projects" | ||||
| ) | ||||
|  | ||||
| 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) | ||||
| 		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}} | ||||
| 				</span> | ||||
| 			</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}} | ||||
| 			<div class="timeline-item-group"> | ||||
| 				<div class="timeline-item event" id="{{.HashTag}}"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user