mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 05:36:19 +01:00
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