| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| } | |||
| @@ -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)}} | |||