Do not reload page after adding comments in Pull Request reviews (#13877)
Fixed #8861 * use ajax on PR review page * handle review comments * extract duplicate code FetchCodeCommentsByLine was initially more or less copied from fetchCodeCommentsByReview. Now they both use a common findCodeComments function instead * use the Engine that was passed into the method Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
parent
ce43d38b4f
commit
bcb7f35221
@ -979,7 +979,7 @@ func (opts *FindCommentsOptions) toConds() builder.Cond {
|
|||||||
if opts.Type != CommentTypeUnknown {
|
if opts.Type != CommentTypeUnknown {
|
||||||
cond = cond.And(builder.Eq{"comment.type": opts.Type})
|
cond = cond.And(builder.Eq{"comment.type": opts.Type})
|
||||||
}
|
}
|
||||||
if opts.Line > 0 {
|
if opts.Line != 0 {
|
||||||
cond = cond.And(builder.Eq{"comment.line": opts.Line})
|
cond = cond.And(builder.Eq{"comment.line": opts.Line})
|
||||||
}
|
}
|
||||||
if len(opts.TreePath) > 0 {
|
if len(opts.TreePath) > 0 {
|
||||||
@ -1078,18 +1078,35 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
|
|||||||
if review == nil {
|
if review == nil {
|
||||||
review = &Review{ID: 0}
|
review = &Review{ID: 0}
|
||||||
}
|
}
|
||||||
//Find comments
|
|
||||||
opts := FindCommentsOptions{
|
opts := FindCommentsOptions{
|
||||||
Type: CommentTypeCode,
|
Type: CommentTypeCode,
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
ReviewID: review.ID,
|
ReviewID: review.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comments, err := findCodeComments(e, opts, issue, currentUser, review)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, comment := range comments {
|
||||||
|
if pathToLineToComment[comment.TreePath] == nil {
|
||||||
|
pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
|
||||||
|
}
|
||||||
|
pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
|
||||||
|
}
|
||||||
|
return pathToLineToComment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCodeComments(e Engine, opts FindCommentsOptions, issue *Issue, currentUser *User, review *Review) ([]*Comment, error) {
|
||||||
|
var comments []*Comment
|
||||||
|
if review == nil {
|
||||||
|
review = &Review{ID: 0}
|
||||||
|
}
|
||||||
conds := opts.toConds()
|
conds := opts.toConds()
|
||||||
if review.ID == 0 {
|
if review.ID == 0 {
|
||||||
conds = conds.And(builder.Eq{"invalidated": false})
|
conds = conds.And(builder.Eq{"invalidated": false})
|
||||||
}
|
}
|
||||||
|
|
||||||
var comments []*Comment
|
|
||||||
if err := e.Where(conds).
|
if err := e.Where(conds).
|
||||||
Asc("comment.created_unix").
|
Asc("comment.created_unix").
|
||||||
Asc("comment.id").
|
Asc("comment.id").
|
||||||
@ -1117,7 +1134,19 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n := 0
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
|
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
||||||
|
// If the review is pending only the author can see the comments (except if the review is set)
|
||||||
|
if review.ID == 0 && re.Type == ReviewTypePending &&
|
||||||
|
(currentUser == nil || currentUser.ID != re.ReviewerID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comment.Review = re
|
||||||
|
}
|
||||||
|
comments[n] = comment
|
||||||
|
n++
|
||||||
|
|
||||||
if err := comment.LoadResolveDoer(); err != nil {
|
if err := comment.LoadResolveDoer(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1126,25 +1155,21 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
|
||||||
// If the review is pending only the author can see the comments (except the review is set)
|
|
||||||
if review.ID == 0 {
|
|
||||||
if re.Type == ReviewTypePending &&
|
|
||||||
(currentUser == nil || currentUser.ID != re.ReviewerID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
comment.Review = re
|
|
||||||
}
|
|
||||||
|
|
||||||
comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
|
comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
|
||||||
issue.Repo.ComposeMetas()))
|
issue.Repo.ComposeMetas()))
|
||||||
if pathToLineToComment[comment.TreePath] == nil {
|
|
||||||
pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
|
|
||||||
}
|
|
||||||
pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
|
|
||||||
}
|
}
|
||||||
return pathToLineToComment, nil
|
return comments[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
|
||||||
|
func FetchCodeCommentsByLine(issue *Issue, currentUser *User, treePath string, line int64) ([]*Comment, error) {
|
||||||
|
opts := FindCommentsOptions{
|
||||||
|
Type: CommentTypeCode,
|
||||||
|
IssueID: issue.ID,
|
||||||
|
TreePath: treePath,
|
||||||
|
Line: line,
|
||||||
|
}
|
||||||
|
return findCodeComments(x, opts, issue, currentUser, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
|
// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
|
||||||
|
@ -548,6 +548,7 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error
|
|||||||
|
|
||||||
// CodeCommentForm form for adding code comments for PRs
|
// CodeCommentForm form for adding code comments for PRs
|
||||||
type CodeCommentForm struct {
|
type CodeCommentForm struct {
|
||||||
|
Origin string `binding:"Required;In(timeline,diff)"`
|
||||||
Content string `binding:"Required"`
|
Content string `binding:"Required"`
|
||||||
Side string `binding:"Required;In(previous,proposed)"`
|
Side string `binding:"Required;In(previous,proposed)"`
|
||||||
Line int64
|
Line int64
|
||||||
|
@ -9,11 +9,40 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplConversation base.TplName = "repo/diff/conversation"
|
||||||
|
tplNewComment base.TplName = "repo/diff/new_comment"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderNewCodeCommentForm will render the form for creating a new review comment
|
||||||
|
func RenderNewCodeCommentForm(ctx *context.Context) {
|
||||||
|
issue := GetActionIssue(ctx)
|
||||||
|
if !issue.IsPull {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentReview, err := models.GetCurrentReview(ctx.User, issue)
|
||||||
|
if err != nil && !models.IsErrReviewNotExist(err) {
|
||||||
|
ctx.ServerError("GetCurrentReview", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["PageIsPullFiles"] = true
|
||||||
|
ctx.Data["Issue"] = issue
|
||||||
|
ctx.Data["CurrentReview"] = currentReview
|
||||||
|
pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRefCommitID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
||||||
|
ctx.HTML(200, tplNewComment)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCodeComment will create a code comment including an pending review if required
|
// CreateCodeComment will create a code comment including an pending review if required
|
||||||
func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
|
func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
|
||||||
issue := GetActionIssue(ctx)
|
issue := GetActionIssue(ctx)
|
||||||
@ -58,11 +87,17 @@ func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
|
log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
|
||||||
|
|
||||||
|
if form.Origin == "diff" {
|
||||||
|
renderConversation(ctx, comment)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Redirect(comment.HTMLURL())
|
ctx.Redirect(comment.HTMLURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateResolveConversation add or remove an Conversation resolved mark
|
// UpdateResolveConversation add or remove an Conversation resolved mark
|
||||||
func UpdateResolveConversation(ctx *context.Context) {
|
func UpdateResolveConversation(ctx *context.Context) {
|
||||||
|
origin := ctx.Query("origin")
|
||||||
action := ctx.Query("action")
|
action := ctx.Query("action")
|
||||||
commentID := ctx.QueryInt64("comment_id")
|
commentID := ctx.QueryInt64("comment_id")
|
||||||
|
|
||||||
@ -103,11 +138,38 @@ func UpdateResolveConversation(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if origin == "diff" {
|
||||||
|
renderConversation(ctx, comment)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.JSON(200, map[string]interface{}{
|
ctx.JSON(200, map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderConversation(ctx *context.Context, comment *models.Comment) {
|
||||||
|
comments, err := models.FetchCodeCommentsByLine(comment.Issue, ctx.User, comment.TreePath, comment.Line)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("FetchCodeCommentsByLine", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["PageIsPullFiles"] = true
|
||||||
|
ctx.Data["comments"] = comments
|
||||||
|
ctx.Data["CanMarkConversation"] = true
|
||||||
|
ctx.Data["Issue"] = comment.Issue
|
||||||
|
if err = comment.Issue.LoadPullRequest(); err != nil {
|
||||||
|
ctx.ServerError("comment.Issue.LoadPullRequest", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRefCommitID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
||||||
|
ctx.HTML(200, tplConversation)
|
||||||
|
}
|
||||||
|
|
||||||
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
|
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
|
||||||
func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
|
func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
|
||||||
issue := GetActionIssue(ctx)
|
issue := GetActionIssue(ctx)
|
||||||
|
@ -856,6 +856,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
|
|||||||
m.Group("/files", func() {
|
m.Group("/files", func() {
|
||||||
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
|
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
|
||||||
m.Group("/reviews", func() {
|
m.Group("/reviews", func() {
|
||||||
|
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
|
||||||
m.Post("/comments", bindIgnErr(auth.CodeCommentForm{}), repo.CreateCodeComment)
|
m.Post("/comments", bindIgnErr(auth.CodeCommentForm{}), repo.CreateCodeComment)
|
||||||
m.Post("/submit", bindIgnErr(auth.SubmitReviewForm{}), repo.SubmitReview)
|
m.Post("/submit", bindIgnErr(auth.SubmitReviewForm{}), repo.SubmitReview)
|
||||||
}, context.RepoMustNotBeArchived())
|
}, context.RepoMustNotBeArchived())
|
||||||
|
@ -144,28 +144,25 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
<div id="pull_review_add_comment" class="hide">
|
<div class="hide" id="edit-content-form">
|
||||||
{{template "repo/diff/new_comment" dict "root" .}}
|
<div class="ui comment form">
|
||||||
|
<div class="ui top attached tabular menu">
|
||||||
|
<a class="active write item">{{$.i18n.Tr "write"}}</a>
|
||||||
|
<a class="preview item" data-url="{{$.Repository.APIURL}}/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hide" id="edit-content-form">
|
<div class="ui bottom attached active write tab segment">
|
||||||
<div class="ui comment form">
|
<textarea class="review-textarea" tabindex="1" name="content"></textarea>
|
||||||
<div class="ui top attached tabular menu">
|
|
||||||
<a class="active write item">{{$.i18n.Tr "write"}}</a>
|
|
||||||
<a class="preview item" data-url="{{$.Repository.APIURL}}/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a>
|
|
||||||
</div>
|
|
||||||
<div class="ui bottom attached active write tab segment">
|
|
||||||
<textarea class="review-textarea" tabindex="1" name="content"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="ui bottom attached tab preview segment markdown">
|
|
||||||
{{$.i18n.Tr "loading"}}
|
|
||||||
</div>
|
|
||||||
<div class="text right edit buttons">
|
|
||||||
<div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div>
|
|
||||||
<div class="ui green save button" tabindex="2">{{.i18n.Tr "repo.issues.save"}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
<div class="ui bottom attached tab preview segment markdown">
|
||||||
|
{{$.i18n.Tr "loading"}}
|
||||||
|
</div>
|
||||||
|
<div class="text right edit buttons">
|
||||||
|
<div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div>
|
||||||
|
<div class="ui green save button" tabindex="2">{{.i18n.Tr "repo.issues.save"}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .IsSplitStyle}}
|
{{if .IsSplitStyle}}
|
||||||
<script>
|
<script>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<form class="ui form {{if $.hidden}}hide comment-form comment-form-reply{{end}}" action="{{$.root.Issue.HTMLURL}}/files/reviews/comments" method="post">
|
<form class="ui form {{if $.hidden}}hide comment-form comment-form-reply{{end}}" action="{{$.root.Issue.HTMLURL}}/files/reviews/comments" method="post">
|
||||||
{{$.root.CsrfTokenHtml}}
|
{{$.root.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="origin" value="{{if $.root.PageIsPullFiles}}diff{{else}}timeline{{end}}">
|
||||||
<input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}"/>
|
<input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}"/>
|
||||||
<input type="hidden" name="side" value="{{if $.Side}}{{$.Side}}{{end}}">
|
<input type="hidden" name="side" value="{{if $.Side}}{{$.Side}}{{end}}">
|
||||||
<input type="hidden" name="line" value="{{if $.Line}}{{$.Line}}{{end}}">
|
<input type="hidden" name="line" value="{{if $.Line}}{{$.Line}}{{end}}">
|
||||||
@ -29,7 +30,7 @@
|
|||||||
<span class="markdown-info">{{svg "octicon-markdown"}} {{$.root.i18n.Tr "repo.diff.comment.markdown_info"}}</span>
|
<span class="markdown-info">{{svg "octicon-markdown"}} {{$.root.i18n.Tr "repo.diff.comment.markdown_info"}}</span>
|
||||||
<div class="ui right">
|
<div class="ui right">
|
||||||
{{if $.reply}}
|
{{if $.reply}}
|
||||||
<button class="ui submit green tiny button btn-reply" onclick="window.submitReply(this);">{{$.root.i18n.Tr "repo.diff.comment.reply"}}</button>
|
<button class="ui submit green tiny button btn-reply" type="submit">{{$.root.i18n.Tr "repo.diff.comment.reply"}}</button>
|
||||||
<input type="hidden" name="reply" value="{{$.reply}}">
|
<input type="hidden" name="reply" value="{{$.reply}}">
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if $.root.CurrentReview}}
|
{{if $.root.CurrentReview}}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{$resolved := (index .comments 0).IsResolved}}
|
{{$resolved := (index .comments 0).IsResolved}}
|
||||||
{{$resolveDoer := (index .comments 0).ResolveDoer}}
|
{{$resolveDoer := (index .comments 0).ResolveDoer}}
|
||||||
{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
|
{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
|
||||||
<div class="conversation-holder">
|
<div class="conversation-holder" data-path="{{(index .comments 0).TreePath}}" data-side="{{if lt (index .comments 0).Line 0}}left{{else}}right{{end}}" data-idx="{{(index .comments 0).UnsignedLine}}">
|
||||||
{{if $resolved}}
|
{{if $resolved}}
|
||||||
<div class="ui attached header resolved-placeholder">
|
<div class="ui attached header resolved-placeholder">
|
||||||
<span class="ui grey text left"><b>{{$resolveDoer.Name}}</b> {{$.i18n.Tr "repo.issues.review.resolved_by"}}</span>
|
<span class="ui grey text left"><b>{{$resolveDoer.Name}}</b> {{$.i18n.Tr "repo.issues.review.resolved_by"}}</span>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}}
|
{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}}
|
||||||
{{if and $.CanMarkConversation $isNotPending}}
|
{{if and $.CanMarkConversation $isNotPending}}
|
||||||
<button class="ui icon tiny button resolve-conversation" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation" >
|
<button class="ui icon tiny button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation" >
|
||||||
{{if $resolved}}
|
{{if $resolved}}
|
||||||
{{$.i18n.Tr "repo.issues.review.un_resolve_conversation"}}
|
{{$.i18n.Tr "repo.issues.review.un_resolve_conversation"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
<div class="field comment-code-cloud">
|
<div class="conversation-holder">
|
||||||
{{template "repo/diff/comment_form_datahandler" .}}
|
<div class="field comment-code-cloud">
|
||||||
|
{{template "repo/diff/comment_form_datahandler" .}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{$file := .file}}
|
{{$file := .file}}
|
||||||
{{range $j, $section := $file.Sections}}
|
{{range $j, $section := $file.Sections}}
|
||||||
{{range $k, $line := $section.Lines}}
|
{{range $k, $line := $section.Lines}}
|
||||||
<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}">
|
<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{DiffLineTypeToStr .GetType}}">
|
||||||
{{if eq .GetType 4}}
|
{{if eq .GetType 4}}
|
||||||
<td class="lines-num lines-num-old">
|
<td class="lines-num lines-num-old">
|
||||||
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5) }}
|
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5) }}
|
||||||
@ -24,14 +24,14 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{Sha1 $file.Name}}L{{$line.LeftIdx}}{{end}}"></span></td>
|
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{Sha1 $file.Name}}L{{$line.LeftIdx}}{{end}}"></span></td>
|
||||||
<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||||
<td class="lines-code lines-code-old halfwidth">{{if and $.root.SignedUserID $line.CanComment $.root.PageIsPullFiles (not (eq .GetType 2))}}<a class="ui primary button add-code-comment add-code-comment-left" data-path="{{$file.Name}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{svg "octicon-plus"}}</a>{{end}}<code class="code-inner">{{if $line.LeftIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></td>
|
<td class="lines-code lines-code-old halfwidth">{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}<a class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} invisible{{end}}" data-path="{{$file.Name}}" data-side="left" data-idx="{{$line.LeftIdx}}" data-new-comment-url="{{$.root.Issue.HTMLURL}}/files/reviews/new_comment">{{svg "octicon-plus"}}</a>{{end}}<code class="code-inner">{{if $line.LeftIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></td>
|
||||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}"></span></td>
|
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}"></span></td>
|
||||||
<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||||
<td class="lines-code lines-code-new halfwidth">{{if and $.root.SignedUserID $line.CanComment $.root.PageIsPullFiles (not (eq .GetType 3))}}<a class="ui primary button add-code-comment add-code-comment-right" data-path="{{$file.Name}}" data-side="right" data-idx="{{$line.RightIdx}}">{{svg "octicon-plus"}}</a>{{end}}<code class="code-inner">{{if $line.RightIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></td>
|
<td class="lines-code lines-code-new halfwidth">{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}<a class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} invisible{{end}}" data-path="{{$file.Name}}" data-side="right" data-idx="{{$line.RightIdx}}" data-new-comment-url="{{$.root.Issue.HTMLURL}}/files/reviews/new_comment">{{svg "octicon-plus"}}</a>{{end}}<code class="code-inner">{{if $line.RightIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></td>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tr>
|
</tr>
|
||||||
{{if gt (len $line.Comments) 0}}
|
{{if gt (len $line.Comments) 0}}
|
||||||
<tr class="add-code-comment">
|
<tr class="add-comment" data-line-type="{{DiffLineTypeToStr .GetType}}">
|
||||||
<td class="lines-num"></td>
|
<td class="lines-num"></td>
|
||||||
<td class="lines-type-marker"></td>
|
<td class="lines-type-marker"></td>
|
||||||
<td class="add-comment-left">
|
<td class="add-comment-left">
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{{range $j, $section := $file.Sections}}
|
{{range $j, $section := $file.Sections}}
|
||||||
{{range $k, $line := $section.Lines}}
|
{{range $k, $line := $section.Lines}}
|
||||||
{{if or $.root.AfterCommitID (ne .GetType 4)}}
|
{{if or $.root.AfterCommitID (ne .GetType 4)}}
|
||||||
<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}">
|
<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{DiffLineTypeToStr .GetType}}">
|
||||||
{{if eq .GetType 4}}
|
{{if eq .GetType 4}}
|
||||||
<td colspan="2" class="lines-num">
|
<td colspan="2" class="lines-num">
|
||||||
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5) }}
|
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5) }}
|
||||||
@ -29,11 +29,11 @@
|
|||||||
{{if eq .GetType 4}}
|
{{if eq .GetType 4}}
|
||||||
<td class="chroma lines-code blob-hunk"><code class="code-inner">{{$section.GetComputedInlineDiffFor $line}}</code></td>
|
<td class="chroma lines-code blob-hunk"><code class="code-inner">{{$section.GetComputedInlineDiffFor $line}}</code></td>
|
||||||
{{else}}
|
{{else}}
|
||||||
<td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">{{if and $.root.SignedUserID $line.CanComment $.root.PageIsPullFiles}}<a class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}" data-path="{{$file.Name}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{svg "octicon-plus"}}</a>{{end}}<code class="code-inner">{{$section.GetComputedInlineDiffFor $line}}</code></td>
|
<td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">{{if and $.root.SignedUserID $.root.PageIsPullFiles}}<a class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} invisible{{end}}" data-path="{{$file.Name}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}" data-new-comment-url="{{$.root.Issue.HTMLURL}}/files/reviews/new_comment">{{svg "octicon-plus"}}</a>{{end}}<code class="code-inner">{{$section.GetComputedInlineDiffFor $line}}</code></td>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tr>
|
</tr>
|
||||||
{{if gt (len $line.Comments) 0}}
|
{{if gt (len $line.Comments) 0}}
|
||||||
<tr>
|
<tr class="add-comment" data-line-type="{{DiffLineTypeToStr .GetType}}">
|
||||||
<td colspan="2" class="lines-num"></td>
|
<td colspan="2" class="lines-num"></td>
|
||||||
<td class="add-comment-left add-comment-right" colspan="2">
|
<td class="add-comment-left add-comment-right" colspan="2">
|
||||||
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
|
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
|
||||||
|
@ -530,7 +530,7 @@
|
|||||||
{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index $comms 0).ReviewID "root" $ "comment" (index $comms 0)}}
|
{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index $comms 0).ReviewID "root" $ "comment" (index $comms 0)}}
|
||||||
|
|
||||||
{{if and $.CanMarkConversation $isNotPending}}
|
{{if and $.CanMarkConversation $isNotPending}}
|
||||||
<button class="ui tiny button resolve-conversation" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index $comms 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation" >
|
<button class="ui tiny button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index $comms 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation" >
|
||||||
{{if $resolved}}
|
{{if $resolved}}
|
||||||
{{$.i18n.Tr "repo.issues.review.un_resolve_conversation"}}
|
{{$.i18n.Tr "repo.issues.review.un_resolve_conversation"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -907,7 +907,7 @@ async function initRepository() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Quote reply
|
// Quote reply
|
||||||
$('.quote-reply').on('click', function (event) {
|
$(document).on('click', '.quote-reply', function (event) {
|
||||||
$(this).closest('.dropdown').find('.menu').toggle('visible');
|
$(this).closest('.dropdown').find('.menu').toggle('visible');
|
||||||
const target = $(this).data('target');
|
const target = $(this).data('target');
|
||||||
const quote = $(`#comment-${target}`).text().replace(/\n/g, '\n> ');
|
const quote = $(`#comment-${target}`).text().replace(/\n/g, '\n> ');
|
||||||
@ -933,7 +933,7 @@ async function initRepository() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Edit issue or comment content
|
// Edit issue or comment content
|
||||||
$('.edit-content').on('click', async function (event) {
|
$(document).on('click', '.edit-content', async function (event) {
|
||||||
$(this).closest('.dropdown').find('.menu').toggle('visible');
|
$(this).closest('.dropdown').find('.menu').toggle('visible');
|
||||||
const $segment = $(this).closest('.header').next();
|
const $segment = $(this).closest('.header').next();
|
||||||
const $editContentZone = $segment.find('.edit-content-zone');
|
const $editContentZone = $segment.find('.edit-content-zone');
|
||||||
@ -1096,7 +1096,7 @@ async function initRepository() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Delete comment
|
// Delete comment
|
||||||
$('.delete-comment').on('click', function () {
|
$(document).on('click', '.delete-comment', function () {
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
if (window.confirm($this.data('locale'))) {
|
if (window.confirm($this.data('locale'))) {
|
||||||
$.post($this.data('url'), {
|
$.post($this.data('url'), {
|
||||||
@ -1105,6 +1105,15 @@ async function initRepository() {
|
|||||||
const $conversationHolder = $this.closest('.conversation-holder');
|
const $conversationHolder = $this.closest('.conversation-holder');
|
||||||
$(`#${$this.data('comment-id')}`).remove();
|
$(`#${$this.data('comment-id')}`).remove();
|
||||||
if ($conversationHolder.length && !$conversationHolder.find('.comment').length) {
|
if ($conversationHolder.length && !$conversationHolder.find('.comment').length) {
|
||||||
|
const path = $conversationHolder.data('path');
|
||||||
|
const side = $conversationHolder.data('side');
|
||||||
|
const idx = $conversationHolder.data('idx');
|
||||||
|
const lineType = $conversationHolder.closest('tr').data('line-type');
|
||||||
|
if (lineType === 'same') {
|
||||||
|
$(`a.add-code-comment[data-path="${path}"][data-idx="${idx}"]`).removeClass('invisible');
|
||||||
|
} else {
|
||||||
|
$(`a.add-code-comment[data-path="${path}"][data-side="${side}"][data-idx="${idx}"]`).removeClass('invisible');
|
||||||
|
}
|
||||||
$conversationHolder.remove();
|
$conversationHolder.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1235,7 +1244,7 @@ function initPullRequestReview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.show-outdated').on('click', function (e) {
|
$(document).on('click', '.show-outdated', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = $(this).data('comment');
|
const id = $(this).data('comment');
|
||||||
$(this).addClass('hide');
|
$(this).addClass('hide');
|
||||||
@ -1244,7 +1253,7 @@ function initPullRequestReview() {
|
|||||||
$(`#hide-outdated-${id}`).removeClass('hide');
|
$(`#hide-outdated-${id}`).removeClass('hide');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.hide-outdated').on('click', function (e) {
|
$(document).on('click', '.hide-outdated', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = $(this).data('comment');
|
const id = $(this).data('comment');
|
||||||
$(this).addClass('hide');
|
$(this).addClass('hide');
|
||||||
@ -1253,7 +1262,7 @@ function initPullRequestReview() {
|
|||||||
$(`#show-outdated-${id}`).removeClass('hide');
|
$(`#show-outdated-${id}`).removeClass('hide');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('button.comment-form-reply').on('click', function (e) {
|
$(document).on('click', 'button.comment-form-reply', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$(this).hide();
|
$(this).hide();
|
||||||
const form = $(this).parent().find('.comment-form');
|
const form = $(this).parent().find('.comment-form');
|
||||||
@ -1284,7 +1293,7 @@ function initPullRequestReview() {
|
|||||||
$(this).closest('.menu').toggle('visible');
|
$(this).closest('.menu').toggle('visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.add-code-comment').on('click', function (e) {
|
$('a.add-code-comment').on('click', async function (e) {
|
||||||
if ($(e.target).hasClass('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
|
if ($(e.target).hasClass('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -1292,18 +1301,13 @@ function initPullRequestReview() {
|
|||||||
const side = $(this).data('side');
|
const side = $(this).data('side');
|
||||||
const idx = $(this).data('idx');
|
const idx = $(this).data('idx');
|
||||||
const path = $(this).data('path');
|
const path = $(this).data('path');
|
||||||
const form = $('#pull_review_add_comment').html();
|
|
||||||
const tr = $(this).closest('tr');
|
const tr = $(this).closest('tr');
|
||||||
|
const lineType = tr.data('line-type');
|
||||||
const oldLineNum = tr.find('.lines-num-old').data('line-num');
|
|
||||||
const newLineNum = tr.find('.lines-num-new').data('line-num');
|
|
||||||
const addCommentKey = `${oldLineNum}|${newLineNum}`;
|
|
||||||
if (document.querySelector(`[data-add-comment-key="${addCommentKey}"]`)) return; // don't add same comment box twice
|
|
||||||
|
|
||||||
let ntr = tr.next();
|
let ntr = tr.next();
|
||||||
if (!ntr.hasClass('add-comment')) {
|
if (!ntr.hasClass('add-comment')) {
|
||||||
ntr = $(`
|
ntr = $(`
|
||||||
<tr class="add-comment" data-add-comment-key="${addCommentKey}">
|
<tr class="add-comment" data-line-type="${lineType}">
|
||||||
${isSplit ? `
|
${isSplit ? `
|
||||||
<td class="lines-num"></td>
|
<td class="lines-num"></td>
|
||||||
<td class="lines-type-marker"></td>
|
<td class="lines-type-marker"></td>
|
||||||
@ -1312,8 +1316,7 @@ function initPullRequestReview() {
|
|||||||
<td class="lines-type-marker"></td>
|
<td class="lines-type-marker"></td>
|
||||||
<td class="add-comment-right"></td>
|
<td class="add-comment-right"></td>
|
||||||
` : `
|
` : `
|
||||||
<td class="lines-num"></td>
|
<td colspan="2" class="lines-num"></td>
|
||||||
<td class="lines-num"></td>
|
|
||||||
<td class="add-comment-left add-comment-right" colspan="2"></td>
|
<td class="add-comment-left add-comment-right" colspan="2"></td>
|
||||||
`}
|
`}
|
||||||
</tr>`);
|
</tr>`);
|
||||||
@ -1322,21 +1325,20 @@ function initPullRequestReview() {
|
|||||||
|
|
||||||
const td = ntr.find(`.add-comment-${side}`);
|
const td = ntr.find(`.add-comment-${side}`);
|
||||||
let commentCloud = td.find('.comment-code-cloud');
|
let commentCloud = td.find('.comment-code-cloud');
|
||||||
if (commentCloud.length === 0) {
|
if (commentCloud.length === 0 && !ntr.find('button[name="is_review"]').length) {
|
||||||
td.html(form);
|
const data = await $.get($(this).data('new-comment-url'));
|
||||||
|
td.html(data);
|
||||||
commentCloud = td.find('.comment-code-cloud');
|
commentCloud = td.find('.comment-code-cloud');
|
||||||
assingMenuAttributes(commentCloud.find('.menu'));
|
assingMenuAttributes(commentCloud.find('.menu'));
|
||||||
|
|
||||||
td.find("input[name='line']").val(idx);
|
td.find("input[name='line']").val(idx);
|
||||||
td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
|
td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
|
||||||
td.find("input[name='path']").val(path);
|
td.find("input[name='path']").val(path);
|
||||||
|
const $textarea = commentCloud.find('textarea');
|
||||||
|
attachTribute($textarea.get(), {mentions: true, emoji: true});
|
||||||
|
const $simplemde = setCommentSimpleMDE($textarea);
|
||||||
|
$textarea.focus();
|
||||||
|
$simplemde.codemirror.focus();
|
||||||
}
|
}
|
||||||
const $textarea = commentCloud.find('textarea');
|
|
||||||
attachTribute($textarea.get(), {mentions: true, emoji: true});
|
|
||||||
|
|
||||||
const $simplemde = setCommentSimpleMDE($textarea);
|
|
||||||
$textarea.focus();
|
|
||||||
$simplemde.codemirror.focus();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2497,17 +2499,24 @@ $(document).ready(async () => {
|
|||||||
$(e).trigger('click');
|
$(e).trigger('click');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.resolve-conversation').on('click', function (e) {
|
$(document).on('click', '.resolve-conversation', async function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = $(this).data('comment-id');
|
const comment_id = $(this).data('comment-id');
|
||||||
|
const origin = $(this).data('origin');
|
||||||
const action = $(this).data('action');
|
const action = $(this).data('action');
|
||||||
const url = $(this).data('update-url');
|
const url = $(this).data('update-url');
|
||||||
|
|
||||||
$.post(url, {
|
const data = await $.post(url, {_csrf: csrf, origin, action, comment_id});
|
||||||
_csrf: csrf,
|
|
||||||
action,
|
if ($(this).closest('.conversation-holder').length) {
|
||||||
comment_id: id,
|
const conversation = $(data);
|
||||||
}).then(reload);
|
$(this).closest('.conversation-holder').replaceWith(conversation);
|
||||||
|
conversation.find('.dropdown').dropdown();
|
||||||
|
initReactionSelector(conversation);
|
||||||
|
initClipboard();
|
||||||
|
} else {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonsClickOnEnter();
|
buttonsClickOnEnter();
|
||||||
@ -3626,6 +3635,28 @@ function initIssueList() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).on('click', 'button[name="is_review"]', (e) => {
|
||||||
|
$(e.target).closest('form').append('<input type="hidden" name="is_review" value="true">');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('submit', '.conversation-holder form', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = $(e.target);
|
||||||
|
const newConversationHolder = $(await $.post(form.attr('action'), form.serialize()));
|
||||||
|
const {path, side, idx} = newConversationHolder.data();
|
||||||
|
|
||||||
|
form.closest('.conversation-holder').replaceWith(newConversationHolder);
|
||||||
|
if (form.closest('tr').data('line-type') === 'same') {
|
||||||
|
$(`a.add-code-comment[data-path="${path}"][data-idx="${idx}"]`).addClass('invisible');
|
||||||
|
} else {
|
||||||
|
$(`a.add-code-comment[data-path="${path}"][data-side="${side}"][data-idx="${idx}"]`).addClass('invisible');
|
||||||
|
}
|
||||||
|
newConversationHolder.find('.dropdown').dropdown();
|
||||||
|
initReactionSelector(newConversationHolder);
|
||||||
|
initClipboard();
|
||||||
|
});
|
||||||
|
|
||||||
window.cancelCodeComment = function (btn) {
|
window.cancelCodeComment = function (btn) {
|
||||||
const form = $(btn).closest('form');
|
const form = $(btn).closest('form');
|
||||||
if (form.length > 0 && form.hasClass('comment-form')) {
|
if (form.length > 0 && form.hasClass('comment-form')) {
|
||||||
@ -3636,13 +3667,6 @@ window.cancelCodeComment = function (btn) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.submitReply = function (btn) {
|
|
||||||
const form = $(btn).closest('form');
|
|
||||||
if (form.length > 0 && form.hasClass('comment-form')) {
|
|
||||||
form.trigger('submit');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onOAuthLoginClick = function () {
|
window.onOAuthLoginClick = function () {
|
||||||
const oauthLoader = $('#oauth2-login-loader');
|
const oauthLoader = $('#oauth2-login-loader');
|
||||||
const oauthNav = $('#oauth2-login-navigator');
|
const oauthNav = $('#oauth2-login-navigator');
|
||||||
|
Loading…
Reference in New Issue
Block a user