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:
		
							parent
							
								
									c69c01d2b6
								
							
						
					
					
						commit
						ac701637b4
					
				
					 36 changed files with 593 additions and 39 deletions
				
			
		|  | @ -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", | ||||
|  |  | |||
|  | @ -50,6 +50,7 @@ const ( | |||
| 	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) | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -66,6 +66,7 @@ type Review struct { | |||
| 	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}} | ||||
|  |  | |||
|  | @ -38,6 +38,33 @@ | |||
| 									<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 | ||||
| 								{{- else if eq .Review.Type 3}}red | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue