mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Add dismiss review feature (#12674)
* Add dismiss review feature
refs:
    https://github.blog/2016-10-12-dismissing-reviews-on-pull-requests/
    https://developer.github.com/v3/pulls/reviews/#dismiss-a-review-for-a-pull-request
* change modal ui and error message
* Add unDismissReview api
Signed-off-by: a1012112796 <1012112796@qq.com>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
			
			
This commit is contained in:
		@@ -111,6 +111,22 @@ func TestAPIPullReview(t *testing.T) {
 | 
			
		||||
	assert.EqualValues(t, "APPROVED", review.State)
 | 
			
		||||
	assert.EqualValues(t, 3, review.CodeCommentsCount)
 | 
			
		||||
 | 
			
		||||
	// test dismiss review
 | 
			
		||||
	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.DismissPullReviewOptions{
 | 
			
		||||
		Message: "test",
 | 
			
		||||
	})
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &review)
 | 
			
		||||
	assert.EqualValues(t, 6, review.ID)
 | 
			
		||||
	assert.EqualValues(t, true, review.Dismissed)
 | 
			
		||||
 | 
			
		||||
	// test dismiss review
 | 
			
		||||
	req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token))
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &review)
 | 
			
		||||
	assert.EqualValues(t, 6, review.ID)
 | 
			
		||||
	assert.EqualValues(t, false, review.Dismissed)
 | 
			
		||||
 | 
			
		||||
	// test DeletePullReview
 | 
			
		||||
	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
 | 
			
		||||
		Body:  "just a comment",
 | 
			
		||||
 
 | 
			
		||||
@@ -26,30 +26,31 @@ type ActionType int
 | 
			
		||||
 | 
			
		||||
// Possible action types.
 | 
			
		||||
const (
 | 
			
		||||
	ActionCreateRepo         ActionType = iota + 1 // 1
 | 
			
		||||
	ActionRenameRepo                               // 2
 | 
			
		||||
	ActionStarRepo                                 // 3
 | 
			
		||||
	ActionWatchRepo                                // 4
 | 
			
		||||
	ActionCommitRepo                               // 5
 | 
			
		||||
	ActionCreateIssue                              // 6
 | 
			
		||||
	ActionCreatePullRequest                        // 7
 | 
			
		||||
	ActionTransferRepo                             // 8
 | 
			
		||||
	ActionPushTag                                  // 9
 | 
			
		||||
	ActionCommentIssue                             // 10
 | 
			
		||||
	ActionMergePullRequest                         // 11
 | 
			
		||||
	ActionCloseIssue                               // 12
 | 
			
		||||
	ActionReopenIssue                              // 13
 | 
			
		||||
	ActionClosePullRequest                         // 14
 | 
			
		||||
	ActionReopenPullRequest                        // 15
 | 
			
		||||
	ActionDeleteTag                                // 16
 | 
			
		||||
	ActionDeleteBranch                             // 17
 | 
			
		||||
	ActionMirrorSyncPush                           // 18
 | 
			
		||||
	ActionMirrorSyncCreate                         // 19
 | 
			
		||||
	ActionMirrorSyncDelete                         // 20
 | 
			
		||||
	ActionApprovePullRequest                       // 21
 | 
			
		||||
	ActionRejectPullRequest                        // 22
 | 
			
		||||
	ActionCommentPull                              // 23
 | 
			
		||||
	ActionPublishRelease                           // 24
 | 
			
		||||
	ActionCreateRepo          ActionType = iota + 1 // 1
 | 
			
		||||
	ActionRenameRepo                                // 2
 | 
			
		||||
	ActionStarRepo                                  // 3
 | 
			
		||||
	ActionWatchRepo                                 // 4
 | 
			
		||||
	ActionCommitRepo                                // 5
 | 
			
		||||
	ActionCreateIssue                               // 6
 | 
			
		||||
	ActionCreatePullRequest                         // 7
 | 
			
		||||
	ActionTransferRepo                              // 8
 | 
			
		||||
	ActionPushTag                                   // 9
 | 
			
		||||
	ActionCommentIssue                              // 10
 | 
			
		||||
	ActionMergePullRequest                          // 11
 | 
			
		||||
	ActionCloseIssue                                // 12
 | 
			
		||||
	ActionReopenIssue                               // 13
 | 
			
		||||
	ActionClosePullRequest                          // 14
 | 
			
		||||
	ActionReopenPullRequest                         // 15
 | 
			
		||||
	ActionDeleteTag                                 // 16
 | 
			
		||||
	ActionDeleteBranch                              // 17
 | 
			
		||||
	ActionMirrorSyncPush                            // 18
 | 
			
		||||
	ActionMirrorSyncCreate                          // 19
 | 
			
		||||
	ActionMirrorSyncDelete                          // 20
 | 
			
		||||
	ActionApprovePullRequest                        // 21
 | 
			
		||||
	ActionRejectPullRequest                         // 22
 | 
			
		||||
	ActionCommentPull                               // 23
 | 
			
		||||
	ActionPublishRelease                            // 24
 | 
			
		||||
	ActionPullReviewDismissed                       // 25
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Action represents user operation type and other information to
 | 
			
		||||
@@ -259,7 +260,7 @@ func (a *Action) GetCreate() time.Time {
 | 
			
		||||
// GetIssueInfos returns a list of issues associated with
 | 
			
		||||
// the action.
 | 
			
		||||
func (a *Action) GetIssueInfos() []string {
 | 
			
		||||
	return strings.SplitN(a.Content, "|", 2)
 | 
			
		||||
	return strings.SplitN(a.Content, "|", 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIssueTitle returns the title of first issue associated
 | 
			
		||||
 
 | 
			
		||||
@@ -157,7 +157,8 @@ func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
 | 
			
		||||
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
 | 
			
		||||
	sess := x.Where("issue_id = ?", pr.IssueID).
 | 
			
		||||
		And("type = ?", ReviewTypeApprove).
 | 
			
		||||
		And("official = ?", true)
 | 
			
		||||
		And("official = ?", true).
 | 
			
		||||
		And("dismissed = ?", false)
 | 
			
		||||
	if protectBranch.DismissStaleApprovals {
 | 
			
		||||
		sess = sess.And("stale = ?", false)
 | 
			
		||||
	}
 | 
			
		||||
@@ -178,6 +179,7 @@ func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullReque
 | 
			
		||||
	rejectExist, err := x.Where("issue_id = ?", pr.IssueID).
 | 
			
		||||
		And("type = ?", ReviewTypeReject).
 | 
			
		||||
		And("official = ?", true).
 | 
			
		||||
		And("dismissed = ?", false).
 | 
			
		||||
		Exist(new(Review))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("MergeBlockedByRejectedReview: %v", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -104,4 +104,4 @@
 | 
			
		||||
  issue_id: 12
 | 
			
		||||
  official: true
 | 
			
		||||
  updated_unix: 1603196749
 | 
			
		||||
  created_unix: 1603196749
 | 
			
		||||
  created_unix: 1603196749
 | 
			
		||||
 
 | 
			
		||||
@@ -99,6 +99,8 @@ const (
 | 
			
		||||
	CommentTypeProject
 | 
			
		||||
	// 31 Project board changed
 | 
			
		||||
	CommentTypeProjectBoard
 | 
			
		||||
	// Dismiss Review
 | 
			
		||||
	CommentTypeDismissReview
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CommentTag defines comment tag type
 | 
			
		||||
 
 | 
			
		||||
@@ -530,7 +530,7 @@ func (issues IssueList) getApprovalCounts(e Engine) (map[int64][]*ReviewCount, e
 | 
			
		||||
	}
 | 
			
		||||
	sess := e.In("issue_id", ids)
 | 
			
		||||
	err := sess.Select("issue_id, type, count(id) as `count`").
 | 
			
		||||
		Where("official = ?", true).
 | 
			
		||||
		Where("official = ? AND dismissed = ?", true, false).
 | 
			
		||||
		GroupBy("issue_id, type").
 | 
			
		||||
		OrderBy("issue_id").
 | 
			
		||||
		Table("review").
 | 
			
		||||
 
 | 
			
		||||
@@ -286,6 +286,8 @@ var migrations = []Migration{
 | 
			
		||||
	NewMigration("Recreate user table to fix default values", recreateUserTableToFixDefaultValues),
 | 
			
		||||
	// v169 -> v170
 | 
			
		||||
	NewMigration("Update DeleteBranch comments to set the old_ref to the commit_sha", commentTypeDeleteBranchUseOldRef),
 | 
			
		||||
	// v170 -> v171
 | 
			
		||||
	NewMigration("Add Dismissed to Review table", addDismissedReviewColumn),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCurrentDBVersion returns the current db version
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								models/migrations/v170.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								models/migrations/v170.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package migrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func addDismissedReviewColumn(x *xorm.Engine) error {
 | 
			
		||||
	type Review struct {
 | 
			
		||||
		Dismissed bool `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := x.Sync2(new(Review)); err != nil {
 | 
			
		||||
		return fmt.Errorf("Sync2: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -234,7 +234,7 @@ func (pr *PullRequest) GetApprovalCounts() ([]*ReviewCount, error) {
 | 
			
		||||
func (pr *PullRequest) getApprovalCounts(e Engine) ([]*ReviewCount, error) {
 | 
			
		||||
	rCounts := make([]*ReviewCount, 0, 6)
 | 
			
		||||
	sess := e.Where("issue_id = ?", pr.IssueID)
 | 
			
		||||
	return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ?", true).GroupBy("issue_id, type").Table("review").Find(&rCounts)
 | 
			
		||||
	return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetApprovers returns the approvers of the pull request
 | 
			
		||||
 
 | 
			
		||||
@@ -63,9 +63,10 @@ type Review struct {
 | 
			
		||||
	IssueID          int64  `xorm:"index"`
 | 
			
		||||
	Content          string `xorm:"TEXT"`
 | 
			
		||||
	// Official is a review made by an assigned approver (counts towards approval)
 | 
			
		||||
	Official bool   `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	CommitID string `xorm:"VARCHAR(40)"`
 | 
			
		||||
	Stale    bool   `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	Official  bool   `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	CommitID  string `xorm:"VARCHAR(40)"`
 | 
			
		||||
	Stale     bool   `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	Dismissed bool   `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
 | 
			
		||||
	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
			
		||||
	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
			
		||||
@@ -466,8 +467,8 @@ func GetReviewersByIssueID(issueID int64) ([]*Review, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get latest review of each reviwer, sorted in order they were made
 | 
			
		||||
	if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
 | 
			
		||||
		issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
 | 
			
		||||
	if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
 | 
			
		||||
		issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
 | 
			
		||||
		Find(&reviews); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -558,6 +559,19 @@ func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DismissReview change the dismiss status of a review
 | 
			
		||||
func DismissReview(review *Review, isDismiss bool) (err error) {
 | 
			
		||||
	if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	review.Dismissed = isDismiss
 | 
			
		||||
 | 
			
		||||
	_, err = x.Cols("dismissed").Update(review)
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InsertReviews inserts review and review comments
 | 
			
		||||
func InsertReviews(reviews []*Review) error {
 | 
			
		||||
	sess := x.NewSession()
 | 
			
		||||
 
 | 
			
		||||
@@ -142,3 +142,13 @@ func TestGetReviewersByIssueID(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDismissReview(t *testing.T) {
 | 
			
		||||
	review1 := AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
 | 
			
		||||
	review2 := AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
 | 
			
		||||
	assert.NoError(t, DismissReview(review1, true))
 | 
			
		||||
	assert.NoError(t, DismissReview(review2, true))
 | 
			
		||||
	assert.NoError(t, DismissReview(review2, true))
 | 
			
		||||
	assert.NoError(t, DismissReview(review2, false))
 | 
			
		||||
	assert.NoError(t, DismissReview(review2, false))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ func ToPullReview(r *models.Review, doer *models.User) (*api.PullReview, error)
 | 
			
		||||
		CommitID:          r.CommitID,
 | 
			
		||||
		Stale:             r.Stale,
 | 
			
		||||
		Official:          r.Official,
 | 
			
		||||
		Dismissed:         r.Dismissed,
 | 
			
		||||
		CodeCommentsCount: r.GetCodeCommentsCount(),
 | 
			
		||||
		Submitted:         r.CreatedUnix.AsTime(),
 | 
			
		||||
		HTMLURL:           r.HTMLURL(),
 | 
			
		||||
 
 | 
			
		||||
@@ -622,6 +622,12 @@ func (f SubmitReviewForm) HasEmptyContent() bool {
 | 
			
		||||
		len(strings.TrimSpace(f.Content)) == 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DismissReviewForm for dismissing stale review by repo admin
 | 
			
		||||
type DismissReviewForm struct {
 | 
			
		||||
	ReviewID int64 `binding:"Required"`
 | 
			
		||||
	Message  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// __________       .__
 | 
			
		||||
// \______   \ ____ |  |   ____ _____    ______ ____
 | 
			
		||||
//  |       _// __ \|  | _/ __ \\__  \  /  ___// __ \
 | 
			
		||||
 
 | 
			
		||||
@@ -275,6 +275,26 @@ func (*actionNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *mode
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*actionNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
 | 
			
		||||
	reviewerName := review.Reviewer.Name
 | 
			
		||||
	if len(review.OriginalAuthor) > 0 {
 | 
			
		||||
		reviewerName = review.OriginalAuthor
 | 
			
		||||
	}
 | 
			
		||||
	if err := models.NotifyWatchers(&models.Action{
 | 
			
		||||
		ActUserID: doer.ID,
 | 
			
		||||
		ActUser:   doer,
 | 
			
		||||
		OpType:    models.ActionPullReviewDismissed,
 | 
			
		||||
		Content:   fmt.Sprintf("%d|%s|%s", review.Issue.Index, reviewerName, comment.Content),
 | 
			
		||||
		RepoID:    review.Issue.Repo.ID,
 | 
			
		||||
		Repo:      review.Issue.Repo,
 | 
			
		||||
		IsPrivate: review.Issue.Repo.IsPrivate,
 | 
			
		||||
		CommentID: comment.ID,
 | 
			
		||||
		Comment:   comment,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		log.Error("NotifyWatchers [%d]: %v", review.Issue.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *actionNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
 | 
			
		||||
	data, err := json.Marshal(commits)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ type Notifier interface {
 | 
			
		||||
	NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*models.User)
 | 
			
		||||
	NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string)
 | 
			
		||||
	NotifyPullRequestPushCommits(doer *models.User, pr *models.PullRequest, comment *models.Comment)
 | 
			
		||||
	NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment)
 | 
			
		||||
 | 
			
		||||
	NotifyCreateIssueComment(doer *models.User, repo *models.Repository,
 | 
			
		||||
		issue *models.Issue, comment *models.Comment, mentions []*models.User)
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,10 @@ func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *models.User, pr *
 | 
			
		||||
func (*NullNotifier) NotifyPullRequestPushCommits(doer *models.User, pr *models.PullRequest, comment *models.Comment) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin
 | 
			
		||||
func (*NullNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NotifyUpdateComment places a place holder function
 | 
			
		||||
func (*NullNotifier) NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) {
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,12 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *models.User, pr *model
 | 
			
		||||
	m.NotifyCreateIssueComment(doer, comment.Issue.Repo, comment.Issue, comment, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mailNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
 | 
			
		||||
	if err := mailer.MailParticipantsComment(comment, models.ActionPullReviewDismissed, review.Issue, []*models.User{}); err != nil {
 | 
			
		||||
		log.Error("MailParticipantsComment: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mailNotifier) NotifyNewRelease(rel *models.Release) {
 | 
			
		||||
	if err := rel.LoadAttributes(); err != nil {
 | 
			
		||||
		log.Error("NotifyNewRelease: %v", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,13 @@ func NotifyPullRequestPushCommits(doer *models.User, pr *models.PullRequest, com
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin
 | 
			
		||||
func NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
 | 
			
		||||
	for _, notifier := range notifiers {
 | 
			
		||||
		notifier.NotifyPullRevieweDismiss(doer, review, comment)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NotifyUpdateComment notifies update comment to notifiers
 | 
			
		||||
func NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) {
 | 
			
		||||
	for _, notifier := range notifiers {
 | 
			
		||||
 
 | 
			
		||||
@@ -161,6 +161,15 @@ func (ns *notificationService) NotifyPullRequestPushCommits(doer *models.User, p
 | 
			
		||||
	_ = ns.issueQueue.Push(opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ns *notificationService) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) {
 | 
			
		||||
	var opts = issueNotificationOpts{
 | 
			
		||||
		IssueID:              review.IssueID,
 | 
			
		||||
		NotificationAuthorID: doer.ID,
 | 
			
		||||
		CommentID:            comment.ID,
 | 
			
		||||
	}
 | 
			
		||||
	_ = ns.issueQueue.Push(opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ns *notificationService) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) {
 | 
			
		||||
	if !removed {
 | 
			
		||||
		var opts = issueNotificationOpts{
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ type PullReview struct {
 | 
			
		||||
	CommitID          string          `json:"commit_id"`
 | 
			
		||||
	Stale             bool            `json:"stale"`
 | 
			
		||||
	Official          bool            `json:"official"`
 | 
			
		||||
	Dismissed         bool            `json:"dismissed"`
 | 
			
		||||
	CodeCommentsCount int             `json:"comments_count"`
 | 
			
		||||
	// swagger:strfmt date-time
 | 
			
		||||
	Submitted time.Time `json:"submitted_at"`
 | 
			
		||||
@@ -92,6 +93,11 @@ type SubmitPullReviewOptions struct {
 | 
			
		||||
	Body  string          `json:"body"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DismissPullReviewOptions are options to dismiss a pull review
 | 
			
		||||
type DismissPullReviewOptions struct {
 | 
			
		||||
	Message string `json:"message"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PullReviewRequestOptions are options to add or remove pull review requests
 | 
			
		||||
type PullReviewRequestOptions struct {
 | 
			
		||||
	Reviewers     []string `json:"reviewers"`
 | 
			
		||||
 
 | 
			
		||||
@@ -798,6 +798,8 @@ func ActionIcon(opType models.ActionType) string {
 | 
			
		||||
		return "diff"
 | 
			
		||||
	case models.ActionPublishRelease:
 | 
			
		||||
		return "tag"
 | 
			
		||||
	case models.ActionPullReviewDismissed:
 | 
			
		||||
		return "x"
 | 
			
		||||
	default:
 | 
			
		||||
		return "question"
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -76,6 +76,7 @@ pull_requests = Pull Requests
 | 
			
		||||
issues = Issues
 | 
			
		||||
milestones = Milestones
 | 
			
		||||
 | 
			
		||||
ok = OK
 | 
			
		||||
cancel = Cancel
 | 
			
		||||
save = Save
 | 
			
		||||
add = Add
 | 
			
		||||
@@ -1104,6 +1105,8 @@ issues.re_request_review=Re-request review
 | 
			
		||||
issues.is_stale = There have been changes to this PR since this review
 | 
			
		||||
issues.remove_request_review=Remove review request
 | 
			
		||||
issues.remove_request_review_block=Can't remove review request
 | 
			
		||||
issues.dismiss_review = Dismiss Review
 | 
			
		||||
issues.dismiss_review_warning = Are you sure you want to dismiss this review?
 | 
			
		||||
issues.sign_in_require_desc = <a href="%s">Sign in</a> to join this conversation.
 | 
			
		||||
issues.edit = Edit
 | 
			
		||||
issues.cancel = Cancel
 | 
			
		||||
@@ -1216,6 +1219,8 @@ issues.review.self.approval = You cannot approve your own pull request.
 | 
			
		||||
issues.review.self.rejection = You cannot request changes on your own pull request.
 | 
			
		||||
issues.review.approve = "approved these changes %s"
 | 
			
		||||
issues.review.comment = "reviewed %s"
 | 
			
		||||
issues.review.dismissed = "dismissed %s’s review %s"
 | 
			
		||||
issues.review.dismissed_label = Dismissed
 | 
			
		||||
issues.review.left_comment = left a comment
 | 
			
		||||
issues.review.content.empty = You need to leave a comment indicating the requested change(s).
 | 
			
		||||
issues.review.reject = "requested changes %s"
 | 
			
		||||
@@ -2519,6 +2524,8 @@ mirror_sync_delete = synced and deleted reference <code>%[2]s</code> at <a href=
 | 
			
		||||
approve_pull_request = `approved <a href="%s/pulls/%s">%s#%[2]s</a>`
 | 
			
		||||
reject_pull_request = `suggested changes for <a href="%s/pulls/%s">%s#%[2]s</a>`
 | 
			
		||||
publish_release  = `released <a href="%s/releases/tag/%s"> "%[4]s" </a> at <a href="%[1]s">%[3]s</a>`
 | 
			
		||||
review_dismissed = `dismissed review from <b>%[4]s</b> for <a href="%[1]s/pulls/%[2]s">%[3]s#%[2]s</a>`
 | 
			
		||||
review_dismissed_reason = Reason:
 | 
			
		||||
create_branch = created branch <a href="%[1]s/src/branch/%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
 | 
			
		||||
 | 
			
		||||
[tool]
 | 
			
		||||
 
 | 
			
		||||
@@ -891,6 +891,8 @@ func Routes() *web.Route {
 | 
			
		||||
									Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
 | 
			
		||||
								m.Combo("/comments").
 | 
			
		||||
									Get(repo.GetPullReviewComments)
 | 
			
		||||
								m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
 | 
			
		||||
								m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
 | 
			
		||||
							})
 | 
			
		||||
						})
 | 
			
		||||
						m.Combo("/requested_reviewers").
 | 
			
		||||
 
 | 
			
		||||
@@ -757,3 +757,129 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DismissPullReview dismiss a review for a pull request
 | 
			
		||||
func DismissPullReview(ctx *context.APIContext) {
 | 
			
		||||
	// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals repository repoDismissPullReview
 | 
			
		||||
	// ---
 | 
			
		||||
	// summary: Dismiss a review for a pull request
 | 
			
		||||
	// produces:
 | 
			
		||||
	// - application/json
 | 
			
		||||
	// parameters:
 | 
			
		||||
	// - name: owner
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: owner of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: repo
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: name of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: index
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: index of the pull request
 | 
			
		||||
	//   type: integer
 | 
			
		||||
	//   format: int64
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: id
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: id of the review
 | 
			
		||||
	//   type: integer
 | 
			
		||||
	//   format: int64
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: body
 | 
			
		||||
	//   in: body
 | 
			
		||||
	//   required: true
 | 
			
		||||
	//   schema:
 | 
			
		||||
	//     "$ref": "#/definitions/DismissPullReviewOptions"
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/PullReview"
 | 
			
		||||
	//   "403":
 | 
			
		||||
	//     "$ref": "#/responses/forbidden"
 | 
			
		||||
	//   "422":
 | 
			
		||||
	//     "$ref": "#/responses/validationError"
 | 
			
		||||
	opts := web.GetForm(ctx).(*api.DismissPullReviewOptions)
 | 
			
		||||
	dismissReview(ctx, opts.Message, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnDismissPullReview cancel to dismiss a review for a pull request
 | 
			
		||||
func UnDismissPullReview(ctx *context.APIContext) {
 | 
			
		||||
	// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals repository repoUnDismissPullReview
 | 
			
		||||
	// ---
 | 
			
		||||
	// summary: Cancel to dismiss a review for a pull request
 | 
			
		||||
	// produces:
 | 
			
		||||
	// - application/json
 | 
			
		||||
	// parameters:
 | 
			
		||||
	// - name: owner
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: owner of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: repo
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: name of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: index
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: index of the pull request
 | 
			
		||||
	//   type: integer
 | 
			
		||||
	//   format: int64
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: id
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: id of the review
 | 
			
		||||
	//   type: integer
 | 
			
		||||
	//   format: int64
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/PullReview"
 | 
			
		||||
	//   "403":
 | 
			
		||||
	//     "$ref": "#/responses/forbidden"
 | 
			
		||||
	//   "422":
 | 
			
		||||
	//     "$ref": "#/responses/validationError"
 | 
			
		||||
	dismissReview(ctx, "", false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
 | 
			
		||||
	if !ctx.Repo.IsAdmin() {
 | 
			
		||||
		ctx.Error(http.StatusForbidden, "", "Must be repo admin")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	review, pr, isWrong := prepareSingleReview(ctx)
 | 
			
		||||
	if isWrong {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
 | 
			
		||||
		ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pr.Issue.IsClosed {
 | 
			
		||||
		ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err := pull_service.DismissReview(review.ID, msg, ctx.User, isDismiss)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if review, err = models.GetReviewByID(review.ID); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// convert response
 | 
			
		||||
	apiReview, err := convert.ToPullReview(review, ctx.User)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.JSON(http.StatusOK, apiReview)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -150,6 +150,9 @@ type swaggerParameterBodies struct {
 | 
			
		||||
	// in:body
 | 
			
		||||
	SubmitPullReviewOptions api.SubmitPullReviewOptions
 | 
			
		||||
 | 
			
		||||
	// in:body
 | 
			
		||||
	DismissPullReviewOptions api.DismissPullReviewOptions
 | 
			
		||||
 | 
			
		||||
	// in:body
 | 
			
		||||
	MigrateRepoOptions api.MigrateRepoOptions
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1364,7 +1364,7 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview {
 | 
			
		||||
		} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
 | 
			
		||||
			comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink,
 | 
			
		||||
				ctx.Repo.Repository.ComposeMetas()))
 | 
			
		||||
			if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
 | 
			
		||||
 
 | 
			
		||||
@@ -223,3 +223,15 @@ func SubmitReview(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DismissReview dismissing stale review by repo admin
 | 
			
		||||
func DismissReview(ctx *context.Context) {
 | 
			
		||||
	form := web.GetForm(ctx).(*auth.DismissReviewForm)
 | 
			
		||||
	comm, err := pull_service.DismissReview(form.ReviewID, form.Message, ctx.User, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("pull_service.DismissReview", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -734,6 +734,7 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
			m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
 | 
			
		||||
			m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
 | 
			
		||||
			m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
 | 
			
		||||
			m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(auth.DismissReviewForm{}), repo.DismissReview)
 | 
			
		||||
			m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
 | 
			
		||||
			m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
 | 
			
		||||
			m.Post("/attachments", repo.UploadIssueAttachment)
 | 
			
		||||
 
 | 
			
		||||
@@ -304,6 +304,8 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
 | 
			
		||||
		name = "reopen"
 | 
			
		||||
	case models.ActionMergePullRequest:
 | 
			
		||||
		name = "merge"
 | 
			
		||||
	case models.ActionPullReviewDismissed:
 | 
			
		||||
		name = "review_dismissed"
 | 
			
		||||
	default:
 | 
			
		||||
		switch commentType {
 | 
			
		||||
		case models.CommentTypeReview:
 | 
			
		||||
 
 | 
			
		||||
@@ -253,3 +253,54 @@ func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issu
 | 
			
		||||
 | 
			
		||||
	return review, comm, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DismissReview dismissing stale review by repo admin
 | 
			
		||||
func DismissReview(reviewID int64, message string, doer *models.User, isDismiss bool) (comment *models.Comment, err error) {
 | 
			
		||||
	review, err := models.GetReviewByID(reviewID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
 | 
			
		||||
		return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = models.DismissReview(review, isDismiss); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !isDismiss {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// load data for notify
 | 
			
		||||
	if err = review.LoadAttributes(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err = review.Issue.LoadPullRequest(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err = review.Issue.LoadAttributes(); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	comment, err = models.CreateComment(&models.CreateCommentOptions{
 | 
			
		||||
		Doer:     doer,
 | 
			
		||||
		Content:  message,
 | 
			
		||||
		Type:     models.CommentTypeDismissReview,
 | 
			
		||||
		ReviewID: review.ID,
 | 
			
		||||
		Issue:    review.Issue,
 | 
			
		||||
		Repo:     review.Issue.Repo,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	comment.Review = review
 | 
			
		||||
	comment.Poster = doer
 | 
			
		||||
	comment.Issue = review.Issue
 | 
			
		||||
 | 
			
		||||
	notification.NotifyPullRevieweDismiss(doer, review, comment)
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,8 @@
 | 
			
		||||
			<b>@{{.Doer.Name}}</b> requested changes on this pull request.
 | 
			
		||||
		{{else if eq .ActionName "review"}}
 | 
			
		||||
			<b>@{{.Doer.Name}}</b> commented on this pull request.
 | 
			
		||||
		{{else if eq .ActionName "review_dismissed"}}
 | 
			
		||||
			<b>@{{.Doer.Name}}</b> dismissed last review from {{.Comment.Review.Reviewer.Name}} for this pull request.
 | 
			
		||||
		{{end}}
 | 
			
		||||
 | 
			
		||||
		{{- if eq .Body ""}}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@
 | 
			
		||||
	 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE,
 | 
			
		||||
	 22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED,
 | 
			
		||||
	 26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST,
 | 
			
		||||
	 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED -->
 | 
			
		||||
	 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED 
 | 
			
		||||
	 32 = DISMISSED_REVIEW -->
 | 
			
		||||
	{{if eq .Type 0}}
 | 
			
		||||
		<div class="timeline-item comment" id="{{.HashTag}}">
 | 
			
		||||
		{{if .OriginalAuthor }}
 | 
			
		||||
@@ -415,6 +416,9 @@
 | 
			
		||||
					{{else}}
 | 
			
		||||
						{{$.i18n.Tr "repo.issues.review.comment" $createdStr | Safe}}
 | 
			
		||||
					{{end}}
 | 
			
		||||
					{{if .Review.Dismissed}}
 | 
			
		||||
						<div class="ui small label">{{$.i18n.Tr "repo.issues.review.dismissed_label"}}</div>
 | 
			
		||||
					{{end}}
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
			{{if .Content}}
 | 
			
		||||
@@ -698,5 +702,44 @@
 | 
			
		||||
			</span>
 | 
			
		||||
		</div>
 | 
			
		||||
		{{end}}
 | 
			
		||||
	{{else if eq .Type 32}}
 | 
			
		||||
		<div class="timeline-item-group">
 | 
			
		||||
			<div class="timeline-item event" id="{{.HashTag}}">
 | 
			
		||||
				<a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
 | 
			
		||||
					<img src="{{.Poster.RelAvatarLink}}">
 | 
			
		||||
				</a>
 | 
			
		||||
				<span class="badge grey">{{svg "octicon-x" 16}}</span>
 | 
			
		||||
				<span class="text grey">
 | 
			
		||||
					<a class="author"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.GetDisplayName}}</a>
 | 
			
		||||
					{{$reviewerName := ""}}
 | 
			
		||||
					{{if eq .Review.OriginalAuthor ""}}
 | 
			
		||||
						{{$reviewerName = .Review.Reviewer.Name}}
 | 
			
		||||
					{{else}}
 | 
			
		||||
						{{$reviewerName = .Review.OriginalAuthor}}
 | 
			
		||||
					{{end}}
 | 
			
		||||
					{{$.i18n.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}}
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
			{{if .Content}}
 | 
			
		||||
				<div class="timeline-item comment">
 | 
			
		||||
					<div class="content">
 | 
			
		||||
						<div class="ui top attached header arrow-top">
 | 
			
		||||
							<span class="text grey">
 | 
			
		||||
								{{$.i18n.Tr "action.review_dismissed_reason"}}
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="ui attached segment">
 | 
			
		||||
							<div class="render-content markdown">
 | 
			
		||||
								{{if .RenderedContent}}
 | 
			
		||||
									{{.RenderedContent|Str2html}}
 | 
			
		||||
								{{else}}
 | 
			
		||||
									<span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span>
 | 
			
		||||
								{{end}}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			{{end}}
 | 
			
		||||
		</div>
 | 
			
		||||
	{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,9 +34,36 @@
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="review-item-right">
 | 
			
		||||
							{{if .Review.Stale}}
 | 
			
		||||
							<span class="ui poping up type-icon text grey" data-content="{{$.i18n.Tr "repo.issues.is_stale"}}">
 | 
			
		||||
								<i class="octicon icon fa-hourglass-end"></i>
 | 
			
		||||
							</span>
 | 
			
		||||
								<span class="ui poping up type-icon text grey" data-content="{{$.i18n.Tr "repo.issues.is_stale"}}">
 | 
			
		||||
									<i class="octicon icon fa-hourglass-end"></i>
 | 
			
		||||
								</span>
 | 
			
		||||
							{{end}}
 | 
			
		||||
							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
 | 
			
		||||
								<a href="#" class="ui grey poping up icon dismiss-review-btn" data-review-id="dismiss-review-{{.Review.ID}}" data-content="{{$.i18n.Tr "repo.issues.dismiss_review"}}">
 | 
			
		||||
									{{svg "octicon-x" 16}}
 | 
			
		||||
								</a>
 | 
			
		||||
								<div class="ui small modal" id="dismiss-review-modal">
 | 
			
		||||
									<div class="header">
 | 
			
		||||
										{{$.i18n.Tr "repo.issues.dismiss_review"}}
 | 
			
		||||
									</div>
 | 
			
		||||
									<div class="content">
 | 
			
		||||
										<div class="ui warning message text left">
 | 
			
		||||
											{{$.i18n.Tr "repo.issues.dismiss_review_warning"}}
 | 
			
		||||
										</div>
 | 
			
		||||
										<form class="ui form dismiss-review-form" id="dismiss-review-{{.Review.ID}}" action="{{$.RepoLink}}/issues/dismiss_review" method="post">
 | 
			
		||||
											{{$.CsrfTokenHtml}}
 | 
			
		||||
											<input type="hidden" name="review_id" value="{{.Review.ID}}">
 | 
			
		||||
											<div class="field">
 | 
			
		||||
												<label for="message">{{$.i18n.Tr "action.review_dismissed_reason"}}</label>
 | 
			
		||||
												<input id="message" name="message">
 | 
			
		||||
											</div>
 | 
			
		||||
											<div class="text right actions">
 | 
			
		||||
												<div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div>
 | 
			
		||||
												<button class="ui red button" type="submit">{{$.i18n.Tr "ok"}}</button>
 | 
			
		||||
											</div>
 | 
			
		||||
										</form>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
							{{end}}
 | 
			
		||||
							<span class="type-icon text {{if eq .Review.Type 1}}green
 | 
			
		||||
								{{- else if eq .Review.Type 2}}grey
 | 
			
		||||
 
 | 
			
		||||
@@ -7761,6 +7761,124 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
          "application/json"
 | 
			
		||||
        ],
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "repository"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Dismiss a review for a pull request",
 | 
			
		||||
        "operationId": "repoDismissPullReview",
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "owner of the repo",
 | 
			
		||||
            "name": "owner",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "name of the repo",
 | 
			
		||||
            "name": "repo",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "integer",
 | 
			
		||||
            "format": "int64",
 | 
			
		||||
            "description": "index of the pull request",
 | 
			
		||||
            "name": "index",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "integer",
 | 
			
		||||
            "format": "int64",
 | 
			
		||||
            "description": "id of the review",
 | 
			
		||||
            "name": "id",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "name": "body",
 | 
			
		||||
            "in": "body",
 | 
			
		||||
            "required": true,
 | 
			
		||||
            "schema": {
 | 
			
		||||
              "$ref": "#/definitions/DismissPullReviewOptions"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "$ref": "#/responses/PullReview"
 | 
			
		||||
          },
 | 
			
		||||
          "403": {
 | 
			
		||||
            "$ref": "#/responses/forbidden"
 | 
			
		||||
          },
 | 
			
		||||
          "422": {
 | 
			
		||||
            "$ref": "#/responses/validationError"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
          "application/json"
 | 
			
		||||
        ],
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "repository"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Cancel to dismiss a review for a pull request",
 | 
			
		||||
        "operationId": "repoUnDismissPullReview",
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "owner of the repo",
 | 
			
		||||
            "name": "owner",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "name of the repo",
 | 
			
		||||
            "name": "repo",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "integer",
 | 
			
		||||
            "format": "int64",
 | 
			
		||||
            "description": "index of the pull request",
 | 
			
		||||
            "name": "index",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "integer",
 | 
			
		||||
            "format": "int64",
 | 
			
		||||
            "description": "id of the review",
 | 
			
		||||
            "name": "id",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "$ref": "#/responses/PullReview"
 | 
			
		||||
          },
 | 
			
		||||
          "403": {
 | 
			
		||||
            "$ref": "#/responses/forbidden"
 | 
			
		||||
          },
 | 
			
		||||
          "422": {
 | 
			
		||||
            "$ref": "#/responses/validationError"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/pulls/{index}/update": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
@@ -13036,6 +13154,17 @@
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "DismissPullReviewOptions": {
 | 
			
		||||
      "description": "DismissPullReviewOptions are options to dismiss a pull review",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "message": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "Message"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "EditAttachmentOptions": {
 | 
			
		||||
      "description": "EditAttachmentOptions options for editing attachments",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
@@ -15199,6 +15328,10 @@
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "CommitID"
 | 
			
		||||
        },
 | 
			
		||||
        "dismissed": {
 | 
			
		||||
          "type": "boolean",
 | 
			
		||||
          "x-go-name": "Dismissed"
 | 
			
		||||
        },
 | 
			
		||||
        "html_url": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "HTMLURL"
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,10 @@
 | 
			
		||||
							{{ $branchLink := .GetBranch | EscapePound | Escape}}
 | 
			
		||||
							{{ $linkText := .Content | RenderEmoji }}
 | 
			
		||||
							{{$.i18n.Tr "action.publish_release" .GetRepoLink $branchLink .ShortRepoPath $linkText | Str2html}}
 | 
			
		||||
						{{else if eq .GetOpType 25}}
 | 
			
		||||
							{{ $index := index .GetIssueInfos 0}}
 | 
			
		||||
							{{ $reviewer := index .GetIssueInfos 1}}
 | 
			
		||||
							{{$.i18n.Tr "action.review_dismissed" .GetRepoLink $index .ShortRepoPath $reviewer | Str2html}}
 | 
			
		||||
						{{end}}
 | 
			
		||||
					</p>
 | 
			
		||||
					{{if or (eq .GetOpType 5) (eq .GetOpType 18)}}
 | 
			
		||||
@@ -111,6 +115,9 @@
 | 
			
		||||
						<p class="text light grey">{{index .GetIssueInfos 1}}</p>
 | 
			
		||||
					{{else if or (eq .GetOpType 12) (eq .GetOpType 13) (eq .GetOpType 14) (eq .GetOpType 15)}}
 | 
			
		||||
						<span class="text truncate issue title">{{.GetIssueTitle | RenderEmoji}}</span>
 | 
			
		||||
					{{else if eq .GetOpType 25}}
 | 
			
		||||
					<p class="text light grey">{{$.i18n.Tr "action.review_dismissed_reason"}}</p>
 | 
			
		||||
						<p class="text light grey">{{index .GetIssueInfos 2 | RenderEmoji}}</p>
 | 
			
		||||
					{{end}}
 | 
			
		||||
					<p class="text italic light grey">{{TimeSince .GetCreate $.i18n.Lang}}</p>
 | 
			
		||||
				</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -677,6 +677,13 @@ function initIssueComments() {
 | 
			
		||||
    return false;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  $('.dismiss-review-btn').on('click', function (e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    const $this = $(this);
 | 
			
		||||
    const $dismissReviewModal = $this.next();
 | 
			
		||||
    $dismissReviewModal.modal('show');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  $(document).on('click', (event) => {
 | 
			
		||||
    const urlTarget = $(':target');
 | 
			
		||||
    if (urlTarget.length === 0) return;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user