Implement "conversation lock" for issue comments (#5073)
This commit is contained in:
		
							parent
							
								
									64ce159a6e
								
							
						
					
					
						commit
						44114b38e6
					
				
					 19 changed files with 435 additions and 4 deletions
				
			
		|  | @ -69,6 +69,10 @@ MAX_FILES = 5 | |||
| ; List of prefixes used in Pull Request title to mark them as Work In Progress | ||||
| WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP] | ||||
| 
 | ||||
| [repository.issue] | ||||
| ; List of reasons why a Pull Request or Issue can be locked | ||||
| LOCK_REASONS=Too heated,Off-topic,Resolved,Spam | ||||
| 
 | ||||
| [ui] | ||||
| ; Number of repositories that are displayed on one explore page | ||||
| EXPLORE_PAGING_NUM = 20 | ||||
|  |  | |||
|  | @ -71,6 +71,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||
| - `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request | ||||
|  title to mark them as Work In Progress | ||||
| 
 | ||||
| ### Repository - Issue (`repository.issue`) | ||||
| - `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked | ||||
| 
 | ||||
| ## UI (`ui`) | ||||
| 
 | ||||
| - `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page. | ||||
|  |  | |||
|  | @ -81,7 +81,7 @@ _Symbols used in table:_ | |||
| | Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ | | ||||
| | Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | ||||
| | Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | ||||
| | Lock Discussion | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | ||||
| | Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | ||||
| | Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | ||||
| | Issue Boards | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | ||||
| | Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | ||||
|  |  | |||
|  | @ -57,6 +57,10 @@ type Issue struct { | |||
| 	Reactions        ReactionList  `xorm:"-"` | ||||
| 	TotalTrackedTime int64         `xorm:"-"` | ||||
| 	Assignees        []*User       `xorm:"-"` | ||||
| 
 | ||||
| 	// IsLocked limits commenting abilities to users on an issue
 | ||||
| 	// with write access
 | ||||
| 	IsLocked bool `xorm:"NOT NULL DEFAULT false"` | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
|  |  | |||
|  | @ -80,6 +80,10 @@ const ( | |||
| 	CommentTypeCode | ||||
| 	// Reviews a pull request by giving general feedback
 | ||||
| 	CommentTypeReview | ||||
| 	// Lock an issue, giving only collaborators access
 | ||||
| 	CommentTypeLock | ||||
| 	// Unlocks a previously locked issue
 | ||||
| 	CommentTypeUnlock | ||||
| ) | ||||
| 
 | ||||
| // CommentTag defines comment tag type
 | ||||
|  |  | |||
							
								
								
									
										51
									
								
								models/issue_lock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								models/issue_lock.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | |||
| // Copyright 2019 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 models | ||||
| 
 | ||||
| // IssueLockOptions defines options for locking and/or unlocking an issue/PR
 | ||||
| type IssueLockOptions struct { | ||||
| 	Doer   *User | ||||
| 	Issue  *Issue | ||||
| 	Reason string | ||||
| } | ||||
| 
 | ||||
| // LockIssue locks an issue. This would limit commenting abilities to
 | ||||
| // users with write access to the repo
 | ||||
| func LockIssue(opts *IssueLockOptions) error { | ||||
| 	return updateIssueLock(opts, true) | ||||
| } | ||||
| 
 | ||||
| // UnlockIssue unlocks a previously locked issue.
 | ||||
| func UnlockIssue(opts *IssueLockOptions) error { | ||||
| 	return updateIssueLock(opts, false) | ||||
| } | ||||
| 
 | ||||
| func updateIssueLock(opts *IssueLockOptions, lock bool) error { | ||||
| 	if opts.Issue.IsLocked == lock { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	opts.Issue.IsLocked = lock | ||||
| 
 | ||||
| 	var commentType CommentType | ||||
| 	if opts.Issue.IsLocked { | ||||
| 		commentType = CommentTypeLock | ||||
| 	} else { | ||||
| 		commentType = CommentTypeUnlock | ||||
| 	} | ||||
| 
 | ||||
| 	if err := UpdateIssueCols(opts.Issue, "is_locked"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err := CreateComment(&CreateCommentOptions{ | ||||
| 		Doer:    opts.Doer, | ||||
| 		Issue:   opts.Issue, | ||||
| 		Repo:    opts.Issue.Repo, | ||||
| 		Type:    commentType, | ||||
| 		Content: opts.Reason, | ||||
| 	}) | ||||
| 	return err | ||||
| } | ||||
|  | @ -213,6 +213,8 @@ var migrations = []Migration{ | |||
| 	NewMigration("rename repo is_bare to repo is_empty", renameRepoIsBareToIsEmpty), | ||||
| 	// v79 -> v80
 | ||||
| 	NewMigration("add can close issues via commit in any branch", addCanCloseIssuesViaCommitInAnyBranch), | ||||
| 	// v80 -> v81
 | ||||
| 	NewMigration("add is locked to issues", addIsLockedToIssues), | ||||
| } | ||||
| 
 | ||||
| // Migrate database to current version
 | ||||
|  |  | |||
							
								
								
									
										18
									
								
								models/migrations/v80.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models/migrations/v80.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| // Copyright 2019 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 "github.com/go-xorm/xorm" | ||||
| 
 | ||||
| func addIsLockedToIssues(x *xorm.Engine) error { | ||||
| 	// Issue see models/issue.go
 | ||||
| 	type Issue struct { | ||||
| 		ID       int64 `xorm:"pk autoincr"` | ||||
| 		IsLocked bool  `xorm:"NOT NULL DEFAULT false"` | ||||
| 	} | ||||
| 
 | ||||
| 	return x.Sync2(new(Issue)) | ||||
| 
 | ||||
| } | ||||
|  | @ -10,6 +10,7 @@ import ( | |||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/routers/utils" | ||||
| 
 | ||||
| 	"github.com/Unknwon/com" | ||||
|  | @ -308,6 +309,32 @@ func (f *ReactionForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi | |||
| 	return validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | ||||
| 
 | ||||
| // IssueLockForm form for locking an issue
 | ||||
| type IssueLockForm struct { | ||||
| 	Reason string `binding:"Required"` | ||||
| } | ||||
| 
 | ||||
| // Validate validates the fields
 | ||||
| func (i *IssueLockForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
| 	return validate(errs, ctx.Data, i, ctx.Locale) | ||||
| } | ||||
| 
 | ||||
| // HasValidReason checks to make sure that the reason submitted in
 | ||||
| // the form matches any of the values in the config
 | ||||
| func (i IssueLockForm) HasValidReason() bool { | ||||
| 	if strings.TrimSpace(i.Reason) == "" { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	for _, v := range setting.Repository.Issue.LockReasons { | ||||
| 		if v == i.Reason { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| //    _____  .__.__                   __
 | ||||
| //   /     \ |__|  |   ____   _______/  |_  ____   ____   ____
 | ||||
| //  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \
 | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ package auth | |||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
|  | @ -39,3 +40,27 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) { | |||
| 		assert.Equal(t, v.expected, v.form.HasEmptyContent()) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestIssueLock_HasValidReason(t *testing.T) { | ||||
| 
 | ||||
| 	// Init settings
 | ||||
| 	_ = setting.Repository | ||||
| 
 | ||||
| 	cases := []struct { | ||||
| 		form     IssueLockForm | ||||
| 		expected bool | ||||
| 	}{ | ||||
| 		{IssueLockForm{""}, true}, // an empty reason is accepted
 | ||||
| 		{IssueLockForm{"Off-topic"}, true}, | ||||
| 		{IssueLockForm{"Too heated"}, true}, | ||||
| 		{IssueLockForm{"Spam"}, true}, | ||||
| 		{IssueLockForm{"Resolved"}, true}, | ||||
| 
 | ||||
| 		{IssueLockForm{"ZZZZ"}, false}, | ||||
| 		{IssueLockForm{"I want to lock this issue"}, false}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, v := range cases { | ||||
| 		assert.Equal(t, v.expected, v.form.HasValidReason()) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -227,6 +227,11 @@ var ( | |||
| 		PullRequest struct { | ||||
| 			WorkInProgressPrefixes []string | ||||
| 		} `ini:"repository.pull-request"` | ||||
| 
 | ||||
| 		// Issue Setting
 | ||||
| 		Issue struct { | ||||
| 			LockReasons []string | ||||
| 		} `ini:"repository.issue"` | ||||
| 	}{ | ||||
| 		AnsiCharset:                             "", | ||||
| 		ForcePrivate:                            false, | ||||
|  | @ -279,6 +284,13 @@ var ( | |||
| 		}{ | ||||
| 			WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, | ||||
| 		}, | ||||
| 
 | ||||
| 		// Issue settings
 | ||||
| 		Issue: struct { | ||||
| 			LockReasons []string | ||||
| 		}{ | ||||
| 			LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","), | ||||
| 		}, | ||||
| 	} | ||||
| 	RepoRootPath string | ||||
| 	ScriptType   = "bash" | ||||
|  |  | |||
|  | @ -780,6 +780,25 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab` | |||
| issues.attachment.download = `Click to download "%s"` | ||||
| issues.subscribe = Subscribe | ||||
| issues.unsubscribe = Unsubscribe | ||||
| issues.lock = Lock conversation | ||||
| issues.unlock = Unlock conversation | ||||
| issues.lock.unknown_reason = Cannot lock an issue with an unknown reason. | ||||
| issues.lock_duplicate = An issue cannot be locked twice. | ||||
| issues.unlock_error = Cannot unlock an issue that is not locked. | ||||
| issues.lock_with_reason = "locked as <strong>%s</strong> and limited conversation to collaborators %s" | ||||
| issues.lock_no_reason = "locked and limited conversation to collaborators %s" | ||||
| issues.unlock_comment = "unlocked this conversation %s" | ||||
| issues.lock_confirm = Lock | ||||
| issues.unlock_confirm = Unlock | ||||
| issues.lock.notice_1 = - Other users can’t add new comments to this issue. | ||||
| issues.lock.notice_2 = - You and other collaborators with access to this repository can still leave comments that others can see. | ||||
| issues.lock.notice_3 = - You can always unlock this issue again in the future. | ||||
| issues.unlock.notice_1 = - Everyone would be able to comment on this issue once more. | ||||
| issues.unlock.notice_2 = - You can always lock this issue again in the future. | ||||
| issues.lock.reason = Reason for locking | ||||
| issues.lock.title = Lock conversation on this issue. | ||||
| issues.unlock.title = Unlock conversation on this issue. | ||||
| issues.comment_on_locked = You cannot comment on a locked issue. | ||||
| issues.tracker = Time Tracker | ||||
| issues.start_tracking_short = Start | ||||
| issues.start_tracking = Start Time Tracking | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
|  | @ -169,6 +170,11 @@ func CreateIssueComment(ctx *context.APIContext, form api.CreateIssueCommentOpti | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin { | ||||
| 		ctx.Error(403, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked"))) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(500, "CreateIssueComment", err) | ||||
|  |  | |||
|  | @ -57,6 +57,23 @@ var ( | |||
| 	} | ||||
| ) | ||||
| 
 | ||||
| // MustAllowUserComment checks to make sure if an issue is locked.
 | ||||
| // If locked and user has permissions to write to the repository,
 | ||||
| // then the comment is allowed, else it is blocked
 | ||||
| func MustAllowUserComment(ctx *context.Context) { | ||||
| 
 | ||||
| 	issue := GetActionIssue(ctx) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin { | ||||
| 		ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked")) | ||||
| 		ctx.Redirect(issue.HTMLURL()) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // MustEnableIssues check if repository enable internal issues
 | ||||
| func MustEnableIssues(ctx *context.Context) { | ||||
| 	if !ctx.Repo.CanRead(models.UnitTypeIssues) && | ||||
|  | @ -898,6 +915,9 @@ func ViewIssue(ctx *context.Context) { | |||
| 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string) | ||||
| 	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID) | ||||
| 	ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) | ||||
| 	ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin) | ||||
| 	ctx.Data["IsRepoIssuesWriter"] = ctx.IsSigned && (ctx.Repo.CanWrite(models.UnitTypeIssues) || ctx.User.IsAdmin) | ||||
| 	ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons | ||||
| 	ctx.HTML(200, tplIssueView) | ||||
| } | ||||
| 
 | ||||
|  | @ -1118,6 +1138,11 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { | |||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { | ||||
| 		ctx.Error(403) | ||||
| 	} | ||||
| 
 | ||||
| 	if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin { | ||||
| 		ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked")) | ||||
| 		ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										71
									
								
								routers/repo/issue_lock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								routers/repo/issue_lock.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| // Copyright 2019 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 repo | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/auth" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| ) | ||||
| 
 | ||||
| // LockIssue locks an issue. This would limit commenting abilities to
 | ||||
| // users with write access to the repo.
 | ||||
| func LockIssue(ctx *context.Context, form auth.IssueLockForm) { | ||||
| 
 | ||||
| 	issue := GetActionIssue(ctx) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if issue.IsLocked { | ||||
| 		ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate")) | ||||
| 		ctx.Redirect(issue.HTMLURL()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !form.HasValidReason() { | ||||
| 		ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason")) | ||||
| 		ctx.Redirect(issue.HTMLURL()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := models.LockIssue(&models.IssueLockOptions{ | ||||
| 		Doer:   ctx.User, | ||||
| 		Issue:  issue, | ||||
| 		Reason: form.Reason, | ||||
| 	}); err != nil { | ||||
| 		ctx.ServerError("LockIssue", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther) | ||||
| } | ||||
| 
 | ||||
| // UnlockIssue unlocks a previously locked issue.
 | ||||
| func UnlockIssue(ctx *context.Context) { | ||||
| 
 | ||||
| 	issue := GetActionIssue(ctx) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !issue.IsLocked { | ||||
| 		ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error")) | ||||
| 		ctx.Redirect(issue.HTMLURL()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := models.UnlockIssue(&models.IssueLockOptions{ | ||||
| 		Doer:  ctx.User, | ||||
| 		Issue: issue, | ||||
| 	}); err != nil { | ||||
| 		ctx.ServerError("UnlockIssue", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther) | ||||
| } | ||||
|  | @ -432,6 +432,13 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 	reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests) | ||||
| 	reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests) | ||||
| 
 | ||||
| 	reqRepoIssueWriter := func(ctx *context.Context) { | ||||
| 		if !ctx.Repo.CanWrite(models.UnitTypeIssues) { | ||||
| 			ctx.Error(403) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// ***** START: Organization *****
 | ||||
| 	m.Group("/org", func() { | ||||
| 		m.Group("", func() { | ||||
|  | @ -574,7 +581,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 					m.Post("/add", repo.AddDependency) | ||||
| 					m.Post("/delete", repo.RemoveDependency) | ||||
| 				}) | ||||
| 				m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment) | ||||
| 				m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(auth.CreateCommentForm{}), repo.NewComment) | ||||
| 				m.Group("/times", func() { | ||||
| 					m.Post("/add", bindIgnErr(auth.AddTimeManuallyForm{}), repo.AddTimeManually) | ||||
| 					m.Group("/stopwatch", func() { | ||||
|  | @ -583,6 +590,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 					}) | ||||
| 				}) | ||||
| 				m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction) | ||||
| 				m.Post("/lock", reqRepoIssueWriter, bindIgnErr(auth.IssueLockForm{}), repo.LockIssue) | ||||
| 				m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue) | ||||
| 			}, context.RepoMustNotBeArchived()) | ||||
| 
 | ||||
| 			m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) | ||||
|  |  | |||
|  | @ -69,7 +69,38 @@ | |||
| 			{{if and .Issue.IsPull (not $.Repository.IsArchived)}} | ||||
| 				{{ template "repo/issue/view_content/pull". }} | ||||
| 			{{end}} | ||||
| 
 | ||||
| 			{{if .IsSigned}} | ||||
| 				{{ if or .IsRepoAdmin .IsRepoIssuesWriter (or (not .Issue.IsLocked)) }} | ||||
| 				<div class="comment form"> | ||||
| 					<a class="avatar" href="{{.SignedUser.HomeLink}}"> | ||||
| 						<img src="{{.SignedUser.RelAvatarLink}}"> | ||||
| 					</a> | ||||
| 					<div class="content"> | ||||
| 						<form class="ui segment form" id="comment-form" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/comments" method="post"> | ||||
| 							{{template "repo/issue/comment_tab" .}} | ||||
| 							{{.CsrfTokenHtml}} | ||||
| 							<input id="status" name="status" type="hidden"> | ||||
| 							<div class="text right"> | ||||
| 								{{if and (or .IsIssueWriter .IsIssuePoster) (not .DisableStatusChange)}} | ||||
| 									{{if .Issue.IsClosed}} | ||||
| 										<div id="status-button" class="ui green basic button" tabindex="6" data-status="{{.i18n.Tr "repo.issues.reopen_issue"}}" data-status-and-comment="{{.i18n.Tr "repo.issues.reopen_comment_issue"}}" data-status-val="reopen"> | ||||
| 											{{.i18n.Tr "repo.issues.reopen_issue"}} | ||||
| 										</div> | ||||
| 									{{else}} | ||||
| 										<div id="status-button" class="ui red basic button" tabindex="6" data-status="{{.i18n.Tr "repo.issues.close_issue"}}" data-status-and-comment="{{.i18n.Tr "repo.issues.close_comment_issue"}}" data-status-val="close"> | ||||
| 											{{.i18n.Tr "repo.issues.close_issue"}} | ||||
| 										</div> | ||||
| 									{{end}} | ||||
| 								{{end}} | ||||
| 								<button class="ui green button" tabindex="5"> | ||||
| 									{{.i18n.Tr "repo.issues.create_comment"}} | ||||
| 								</button> | ||||
| 							</div> | ||||
| 						</form> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				{{ end }} | ||||
| 			{{else}} | ||||
| 			{{if .Repository.IsArchived}} | ||||
| 				<div class="ui warning message"> | ||||
| 					{{if .Issue.IsPull}} | ||||
|  | @ -114,6 +145,7 @@ | |||
| 					</div> | ||||
| 				{{end}} | ||||
| 			{{end}} | ||||
| 		{{end}} | ||||
| 		</ui> | ||||
| 	</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,11 @@ | |||
| {{range .Issue.Comments}} | ||||
| 	{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }} | ||||
| 
 | ||||
| 	<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE, 22 = REVIEW --> | ||||
| 	<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, | ||||
| 	 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, | ||||
| 	 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, | ||||
| 	 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE, | ||||
| 	 22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED --> | ||||
| 	{{if eq .Type 0}} | ||||
| 		<div class="comment" id="{{.HashTag}}"> | ||||
| 			<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}> | ||||
|  | @ -355,5 +359,35 @@ | |||
| 				{{end}} | ||||
| 			{{end}} | ||||
| 	    </div> | ||||
| 	{{else if eq .Type 23}} | ||||
| 		<div class="event"> | ||||
| 			<span class="octicon octicon-lock" | ||||
| 				style="font-size:20px;margin-left:-28.5px; margin-right: -1px"></span> | ||||
| 			<a class="ui avatar image" href="{{.Poster.HomeLink}}"> | ||||
| 				<img src="{{.Poster.RelAvatarLink}}"> | ||||
| 			</a> | ||||
| 
 | ||||
| 			{{ if .Content }} | ||||
| 	    		<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> | ||||
| 				{{$.i18n.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} | ||||
| 	    		</span> | ||||
| 			{{ else }} | ||||
| 	    		<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> | ||||
| 				{{$.i18n.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} | ||||
| 	    		</span> | ||||
| 			{{ end }} | ||||
| 		</div> | ||||
| 	{{else if eq .Type 24}} | ||||
| 		<div class="event"> | ||||
| 			<span class="octicon octicon-key" | ||||
| 				style="font-size:20px;margin-left:-28.5px; margin-right: -1px"></span> | ||||
| 			<a class="ui avatar image" href="{{.Poster.HomeLink}}"> | ||||
| 				<img src="{{.Poster.RelAvatarLink}}"> | ||||
| 			</a> | ||||
| 
 | ||||
| 	    		<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> | ||||
| 	    			{{$.i18n.Tr "repo.issues.unlock_comment" $createdStr | Safe}} | ||||
| 	    		</span> | ||||
| 		</div> | ||||
| 	{{end}} | ||||
| {{end}} | ||||
|  |  | |||
|  | @ -335,6 +335,91 @@ | |||
| 					</div> | ||||
| 				{{end}} | ||||
| 			</div> | ||||
| 
 | ||||
| 			{{ if .IsRepoAdmin }} | ||||
| 			<div class="ui divider"></div> | ||||
| 			<div class="ui watching"> | ||||
| 				<div> | ||||
| 					<button class="fluid ui  show-modal button {{if .Issue.IsLocked }} negative {{ end }}" data-modal="#lock"> | ||||
| 							{{if .Issue.IsLocked}} | ||||
| 								<i class="octicon octicon-key"></i> | ||||
| 								{{.i18n.Tr "repo.issues.unlock"}} | ||||
| 							{{else}} | ||||
| 								<i class="octicon octicon-lock"></i> | ||||
| 								{{.i18n.Tr "repo.issues.lock"}} | ||||
| 							{{end}} | ||||
| 						</button> | ||||
| 					</form> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 
 | ||||
| 			<div class="ui tiny modal" id="lock"> | ||||
| 				<div class="header"> | ||||
| 					{{ if .Issue.IsLocked }} | ||||
| 						{{.i18n.Tr "repo.issues.unlock.title"}} | ||||
| 					{{ else }} | ||||
| 						{{.i18n.Tr "repo.issues.lock.title"}} | ||||
| 					{{ end }} | ||||
| 				</div> | ||||
| 			<div class="content"> | ||||
| 				<div class="ui warning message text left"> | ||||
| 					{{ if .Issue.IsLocked }} | ||||
| 						{{.i18n.Tr "repo.issues.unlock.notice_1"}}<br> | ||||
| 						{{.i18n.Tr "repo.issues.unlock.notice_2"}}<br> | ||||
| 					{{ else }} | ||||
| 						{{.i18n.Tr "repo.issues.lock.notice_1"}}<br> | ||||
| 						{{.i18n.Tr "repo.issues.lock.notice_2"}}<br> | ||||
| 						{{.i18n.Tr "repo.issues.lock.notice_3"}}<br> | ||||
| 					{{ end }} | ||||
| 				</div> | ||||
| 
 | ||||
| 				<form class="ui form" action="{{$.RepoLink}}/issues/{{.Issue.Index}}{{ if .Issue.IsLocked }}/unlock{{ else }}/lock{{ end }}" | ||||
| 					method="post"> | ||||
| 					{{.CsrfTokenHtml}} | ||||
| 
 | ||||
| 					{{ if not .Issue.IsLocked }} | ||||
| 					<div class="field"> | ||||
| 						<strong> {{ .i18n.Tr "repo.issues.lock.reason" }} </strong> | ||||
| 					</div> | ||||
| 
 | ||||
| 					<div class="field"> | ||||
| 						<div class="ui fluid dropdown selection" tabindex="0"> | ||||
| 
 | ||||
| 							<select name="reason"> | ||||
| 								<option value=""> </option> | ||||
| 								{{range .LockReasons}} | ||||
| 									<option value="{{.}}">{{.}}</option> | ||||
| 								{{end}} | ||||
| 							</select> | ||||
| 							<i class="dropdown icon"></i> | ||||
| 
 | ||||
| 							<div class="default text"> </div> | ||||
| 
 | ||||
| 							<div class="menu transition hidden" tabindex="-1" style="display: block !important;"> | ||||
| 								{{range .LockReasons}} | ||||
| 									<div class="item" data-value="{{.}}">{{.}}</div> | ||||
| 								{{end}} | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					{{ end }} | ||||
| 
 | ||||
| 					<div class="text right actions"> | ||||
| 						<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div> | ||||
| 						<button class="ui red button"> | ||||
| 							{{ if .Issue.IsLocked }} | ||||
| 								{{.i18n.Tr "repo.issues.unlock_confirm"}} | ||||
| 							{{ else }} | ||||
| 								{{.i18n.Tr "repo.issues.lock_confirm"}} | ||||
| 							{{ end }} | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</form> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		{{ end }} | ||||
| 
 | ||||
| 	</div> | ||||
| </div> | ||||
| {{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}} | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue