diff --git a/models/issues/comment.go b/models/issues/comment.go index c6c5dc2432..d6e5504d72 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -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"` + ProjectColumnTitle string `json:"project_column_name"` + ProjectTitle string `json:"project_name"` +} + // 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.ProjectColumnID > 0 { + commentMetaData = &CommentMetaData{ + ProjectColumnID: opts.ProjectColumnID, + ProjectColumnTitle: opts.ProjectColumnTitle, + ProjectTitle: opts.ProjectColumnTitle, + } + } + 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. diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index f8ee271a6b..9c0d3bb1c9 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -434,6 +434,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 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 08882fb119..353dd5b927 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -591,6 +591,8 @@ var migrations = []Migration{ // v299 -> v300 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 diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go index c8b1593286..14bcaeada8 100644 --- a/models/migrations/v1_22/v287.go +++ b/models/migrations/v1_22/v287.go @@ -1,6 +1,5 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT - package v1_22 //nolint import ( diff --git a/models/migrations/v1_23/v300.go b/models/migrations/v1_23/v300.go new file mode 100644 index 0000000000..acb4e32c41 --- /dev/null +++ b/models/migrations/v1_23/v300.go @@ -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)) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 772b11c2ba..9077fe8041 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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_milestone_at = `added this to the %s milestone %s` issues.add_project_at = `added this to the %s project %s` +issues.move_to_column_of_project = `moved this to %s in %s on %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.remove_milestone_at = `removed this from the %s milestone %s` diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 8fb8f2540f..75ceed459f 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -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 ( @@ -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) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index ce459f23b9..81c6aa9f40 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1680,6 +1680,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) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 9ce5535a0e..84bcb5ead5 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -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 ( @@ -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) return } diff --git a/services/projects/issue.go b/services/projects/issue.go new file mode 100644 index 0000000000..20b44552c4 --- /dev/null +++ b/services/projects/issue.go @@ -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 + }) +} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 3da2f3815e..7d15898b73 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -600,6 +600,22 @@ {{end}} + {{else if eq .Type 31}} + {{if not $.UnitProjectsGlobalDisabled}} +
+ {{svg "octicon-project"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$newProjectDisplayHtml := "Unknown Project"}} + {{if .Project}} + {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} + {{$newProjectDisplayHtml = printf `%s %s` (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}} + +
+ {{end}} {{else if eq .Type 32}}