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