diff --git a/models/issues/comment.go b/models/issues/comment.go index c6c5dc243..48b8e335d 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,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. diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 2c007c72e..22a4548ad 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -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 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index a57b4da03..a3264160e 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go new file mode 100644 index 000000000..adfe917d3 --- /dev/null +++ b/models/migrations/v1_23/v303.go @@ -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)) +} diff --git a/models/project/issue.go b/models/project/issue.go index 3361b533b..1c31b154c 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -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") diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 92f955c78..cca068a3a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 %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 eea539f6d..66760d31d 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 ( @@ -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 } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 1018e88f1..4773cc9ad 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index fdeead570..aac8997d6 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 ( @@ -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 } diff --git a/services/projects/issue.go b/services/projects/issue.go new file mode 100644 index 000000000..db1621a39 --- /dev/null +++ b/services/projects/issue.go @@ -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 + }) +} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 804cd6a2f..1cf928711 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -604,6 +604,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}} + {{$newProjectDisplay := .CommentMetaData.ProjectTitle}} + {{if .Project}} + {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} + {{$newProjectDisplay = HTMLFormat `%s %s` (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}} + +
+ {{end}} {{else if eq .Type 32}}