36 changed files with 1295 additions and 8 deletions
@ -0,0 +1,134 @@ |
|||||||
|
// Copyright 2018-2022 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 |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
) |
||||||
|
|
||||||
|
// IssueParent represents an issue parent
|
||||||
|
type IssueParent struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
UserID int64 `xorm:"NOT NULL"` |
||||||
|
IssueID int64 `xorm:"UNIQUE(issue_parent) NOT NULL"` |
||||||
|
ParentID int64 `xorm:"UNIQUE(issue_parent) NOT NULL"` |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"created"` |
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"` |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
db.RegisterModel(new(IssueParent)) |
||||||
|
} |
||||||
|
|
||||||
|
// ParentType Defines Parent Type Constants
|
||||||
|
type ParentType int |
||||||
|
|
||||||
|
// Define Parent Types
|
||||||
|
const ( |
||||||
|
ParentTypeFather ParentType = iota |
||||||
|
ParentTypeChild |
||||||
|
) |
||||||
|
|
||||||
|
// CreateIssueParent creates a new parent for an issue
|
||||||
|
func CreateIssueParent(user *user_model.User, issue, parent *Issue) error { |
||||||
|
ctx, committer, err := db.TxContext() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer committer.Close() |
||||||
|
sess := db.GetEngine(ctx) |
||||||
|
|
||||||
|
// Check if it aleready exists
|
||||||
|
exists, err := issueParentExists(sess, issue.ID, parent.ID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if exists { |
||||||
|
return ErrParentExists{issue.ID, parent.ID} |
||||||
|
} |
||||||
|
// And if it would be circular
|
||||||
|
circular, err := issueParentExists(sess, parent.ID, issue.ID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if circular { |
||||||
|
return ErrCircularParent{issue.ID, parent.ID} |
||||||
|
} |
||||||
|
|
||||||
|
if err := db.Insert(ctx, &IssueParent{ |
||||||
|
UserID: user.ID, |
||||||
|
IssueID: issue.ID, |
||||||
|
ParentID: parent.ID, |
||||||
|
}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Add comment referencing the new parent
|
||||||
|
if err = createIssueParentComment(ctx, user, issue, parent, true); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return committer.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveIssueParent removes a parent from an issue
|
||||||
|
func RemoveIssueParent(user *user_model.User, issue, parent *Issue, parentType ParentType) (err error) { |
||||||
|
ctx, committer, err := db.TxContext() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer committer.Close() |
||||||
|
|
||||||
|
var issueParentToDelete IssueParent |
||||||
|
|
||||||
|
switch parentType { |
||||||
|
case ParentTypeFather: |
||||||
|
issueParentToDelete = IssueParent{IssueID: issue.ID, ParentID: parent.ID} |
||||||
|
case ParentTypeChild: |
||||||
|
issueParentToDelete = IssueParent{IssueID: parent.ID, ParentID: issue.ID} |
||||||
|
default: |
||||||
|
return ErrUnknownParentType{parentType} |
||||||
|
} |
||||||
|
|
||||||
|
affected, err := db.GetEngine(ctx).Delete(&issueParentToDelete) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// If we deleted nothing, the parent did not exist
|
||||||
|
if affected <= 0 { |
||||||
|
return ErrParentNotExists{issue.ID, parent.ID} |
||||||
|
} |
||||||
|
|
||||||
|
// Add comment referencing the removed parent
|
||||||
|
if err = createIssueParentComment(ctx, user, issue, parent, false); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return committer.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the parent already exists
|
||||||
|
func issueParentExists(e db.Engine, issueID, depID int64) (bool, error) { |
||||||
|
return e.Where("(issue_id = ? AND parent_id = ?)", issueID, depID).Exist(&IssueParent{}) |
||||||
|
} |
||||||
|
|
||||||
|
// IssueNoParentsLeft checks if issue can be closed
|
||||||
|
func IssueNoParentsLeft(issue *Issue) (bool, error) { |
||||||
|
return issueNoParentsLeft(db.GetEngine(db.DefaultContext), issue) |
||||||
|
} |
||||||
|
|
||||||
|
func issueNoParentsLeft(e db.Engine, issue *Issue) (bool, error) { |
||||||
|
exists, err := e. |
||||||
|
Table("issue_parent"). |
||||||
|
Select("issue.*"). |
||||||
|
Join("INNER", "issue", "issue.id = issue_parent.parent_id"). |
||||||
|
Where("issue_parent.issue_id = ?", issue.ID). |
||||||
|
And("issue.is_closed = ?", "0"). |
||||||
|
Exist(&Issue{}) |
||||||
|
|
||||||
|
return !exists, err |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
// Copyright 2018 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 |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestCreateIssueParent(t *testing.T) { |
||||||
|
// Prepare
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
user1, err := user_model.GetUserByID(1) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
issue1, err := GetIssueByID(1) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
issue2, err := GetIssueByID(2) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// Create a dependency and check if it was successful
|
||||||
|
err = CreateIssueParent(user1, issue1, issue2) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
// Do it again to see if it will check if the dependency already exists
|
||||||
|
err = CreateIssueParent(user1, issue1, issue2) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.True(t, IsErrParentExists(err)) |
||||||
|
|
||||||
|
// Check for circular dependencies
|
||||||
|
err = CreateIssueParent(user1, issue2, issue1) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.True(t, IsErrCircularParent(err)) |
||||||
|
|
||||||
|
_ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddParent, PosterID: user1.ID, IssueID: issue1.ID}) |
||||||
|
|
||||||
|
// Check if dependencies left is correct
|
||||||
|
left, err := IssueNoParentsLeft(issue1) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.False(t, left) |
||||||
|
|
||||||
|
// Close #2 and check again
|
||||||
|
_, err = issue2.ChangeStatus(user1, true) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
left, err = IssueNoParentsLeft(issue1) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.True(t, left) |
||||||
|
|
||||||
|
// Test removing the dependency
|
||||||
|
err = RemoveIssueParent(user1, issue1, issue2, ParentTypeFather) |
||||||
|
assert.NoError(t, err) |
||||||
|
} |
@ -0,0 +1,129 @@ |
|||||||
|
// Copyright 2018-2022 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/context" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
) |
||||||
|
|
||||||
|
// AddParent adds new parents
|
||||||
|
func AddParent(ctx *context.Context) { |
||||||
|
issueIndex := ctx.ParamsInt64("index") |
||||||
|
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("GetIssueByIndex", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the Repo is allowed to have parents
|
||||||
|
if !ctx.Repo.CanCreateIssueParents(ctx.User, issue.IsPull) { |
||||||
|
ctx.Error(http.StatusForbidden, "CanCreateIssueParents") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
parentID := ctx.FormInt64("newParent") |
||||||
|
|
||||||
|
if err = issue.LoadRepo(); err != nil { |
||||||
|
ctx.ServerError("LoadRepo", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Redirect
|
||||||
|
defer ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther) |
||||||
|
|
||||||
|
// Parent
|
||||||
|
parent, err := models.GetIssueByID(parentID) |
||||||
|
if err != nil { |
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_issue_not_exist")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if both issues are in the same repo if cross repository parents is not enabled
|
||||||
|
if issue.RepoID != parent.RepoID && !setting.Service.AllowCrossRepositoryParents { |
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_not_same_repo")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if issue and parent is the same
|
||||||
|
if parent.ID == issue.ID { |
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_same_issue")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
err = models.CreateIssueParent(ctx.User, issue, parent) |
||||||
|
if err != nil { |
||||||
|
if models.IsErrParentExists(err) { |
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_exists")) |
||||||
|
return |
||||||
|
} else if models.IsErrCircularParent(err) { |
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_cannot_create_circular")) |
||||||
|
return |
||||||
|
} else { |
||||||
|
ctx.ServerError("CreateOrUpdateIssueParent", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveParent removes the parent
|
||||||
|
func RemoveParent(ctx *context.Context) { |
||||||
|
issueIndex := ctx.ParamsInt64("index") |
||||||
|
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("GetIssueByIndex", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the Repo is allowed to have dependencies
|
||||||
|
if !ctx.Repo.CanCreateIssueParents(ctx.User, issue.IsPull) { |
||||||
|
ctx.Error(http.StatusForbidden, "CanCreateIssueParents") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
parentID := ctx.FormInt64("removeParentID") |
||||||
|
|
||||||
|
if err = issue.LoadRepo(); err != nil { |
||||||
|
ctx.ServerError("LoadRepo", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Parent Type
|
||||||
|
parentTypeStr := ctx.Req.PostForm.Get("parentType") |
||||||
|
|
||||||
|
var parentType models.ParentType |
||||||
|
|
||||||
|
switch parentTypeStr { |
||||||
|
case "father": |
||||||
|
parentType = models.ParentTypeFather |
||||||
|
case "child": |
||||||
|
parentType = models.ParentTypeChild |
||||||
|
default: |
||||||
|
ctx.Error(http.StatusBadRequest, "GetDependecyType") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Parent
|
||||||
|
parent, err := models.GetIssueByID(parentID) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("GetIssueByID", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err = models.RemoveIssueParent(ctx.User, issue, parent, parentType); err != nil { |
||||||
|
if models.IsErrParentNotExists(err) { |
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_not_exist")) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.ServerError("RemoveIssueParent", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Redirect
|
||||||
|
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther) |
||||||
|
} |
@ -1,4 +1,5 @@ |
|||||||
<div class="ui compact left small menu"> |
<div class="ui compact left small menu"> |
||||||
<a class="{{if .PageIsLabels}}active{{end}} item" href="{{.RepoLink}}/labels">{{.i18n.Tr "repo.labels"}}</a> |
<a class="{{if .PageIsLabels}}active{{end}} item" href="{{.RepoLink}}/labels">{{.i18n.Tr "repo.labels"}}</a> |
||||||
<a class="{{if .PageIsMilestones}}active{{end}} item" href="{{.RepoLink}}/milestones">{{.i18n.Tr "repo.milestones"}}</a> |
<a class="{{if .PageIsMilestones}}active{{end}} item" href="{{.RepoLink}}/milestones">{{.i18n.Tr "repo.milestones"}}</a> |
||||||
|
<a class="{{if .PageIsIssuesTree}}active{{end}} item" href="{{.RepoLink}}/issues_tree">{{.i18n.Tr "repo.issues_tree"}}</a> |
||||||
</div> |
</div> |
||||||
|
@ -0,0 +1,293 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
<div class="page-content repository"> |
||||||
|
{{template "repo/header" .}} |
||||||
|
<div class="ui container"> |
||||||
|
<div class="ui three column stackable grid"> |
||||||
|
<div class="column"> |
||||||
|
{{template "repo/issue/navbar" .}} |
||||||
|
</div> |
||||||
|
<div class="column center aligned"> |
||||||
|
{{template "repo/issue/search" .}} |
||||||
|
</div> |
||||||
|
{{if not .Repository.IsArchived}} |
||||||
|
<div class="column right aligned"> |
||||||
|
{{if .PageIsIssuesTree}} |
||||||
|
<a class="ui green button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{.i18n.Tr "repo.issues.new"}}</a> |
||||||
|
{{else}} |
||||||
|
<a class="ui green button {{if not .PullRequestCtx.Allowed}}disabled{{end}}" href="{{if .PullRequestCtx.Allowed}}{{.Repository.Link}}/compare/{{.Repository.DefaultBranch | PathEscapeSegments}}...{{if ne .Repository.Owner.Name .PullRequestCtx.BaseRepo.Owner.Name}}{{PathEscape .Repository.Owner.Name}}:{{end}}{{.Repository.DefaultBranch | PathEscapeSegments}}{{end}}">{{.i18n.Tr "repo.pulls.new"}}</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
{{if not .PageIsIssuesTree}} |
||||||
|
<div class="column right aligned"> |
||||||
|
<a class="ui green button {{if not .PullRequestCtx.Allowed}}disabled{{end}}" href="{{if .PullRequestCtx.Allowed}}{{.PullRequestCtx.BaseRepo.Link}}/compare/{{.PullRequestCtx.BaseRepo.DefaultBranch | PathEscapeSegments}}...{{if ne .Repository.Owner.Name .PullRequestCtx.BaseRepo.Owner.Name}}{{PathEscape .Repository.Owner.Name}}:{{end}}{{.Repository.DefaultBranch | PathEscapeSegments}}{{end}}">{{$.i18n.Tr "action.compare_commits_general"}}</a> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
<div class="ui divider"></div> |
||||||
|
<div id="issue-filters" class="ui stackable grid"> |
||||||
|
<div class="six wide column"> |
||||||
|
{{template "repo/issue/openclose" .}} |
||||||
|
</div> |
||||||
|
<div class="ten wide right aligned column"> |
||||||
|
<div class="ui secondary filter stackable menu labels"> |
||||||
|
<!-- Label --> |
||||||
|
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter" style="margin-left: auto"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.filter_label"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<span class="info">{{.i18n.Tr "repo.issues.filter_label_exclude" | Safe}}</span> |
||||||
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_label_no_select"}}</a> |
||||||
|
{{range .Labels}} |
||||||
|
<a class="item label-filter-item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}" data-label-id="{{.ID}}">{{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if .IsSelected}}{{svg "octicon-check"}}{{end}}<span class="label color" style="background-color: {{.Color}}"></span> {{.Name | RenderEmoji}}</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Milestone --> |
||||||
|
<div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.filter_milestone"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_milestone_no_select"}}</a> |
||||||
|
{{range .Milestones}} |
||||||
|
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected{{end}}{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&assignee={{$.AssigneeID}}">{{.Name}}</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Assignee --> |
||||||
|
<div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.filter_assignee"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}">{{.i18n.Tr "repo.issues.filter_assginee_no_select"}}</a> |
||||||
|
{{range .Assignees}} |
||||||
|
<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{.ID}}"> |
||||||
|
{{avatar .}} {{.GetDisplayName}} |
||||||
|
</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{if .IsSigned}} |
||||||
|
<!-- Type --> |
||||||
|
<div class="ui dropdown type jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.filter_type"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a> |
||||||
|
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a> |
||||||
|
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a> |
||||||
|
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a> |
||||||
|
{{if .PageIsPullList}} |
||||||
|
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.review_requested"}}</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
|
||||||
|
<!-- Sort --> |
||||||
|
<div class="ui dropdown type jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.filter_sort"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a> |
||||||
|
<a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a> |
||||||
|
<a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a> |
||||||
|
<a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a> |
||||||
|
<a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a> |
||||||
|
<a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a> |
||||||
|
<a class="{{if eq .SortType "nearduedate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.nearduedate"}}</a> |
||||||
|
<a class="{{if eq .SortType "farduedate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.farduedate"}}</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div id="issue-actions" class="ui stackable grid hide"> |
||||||
|
<div class="six wide column"> |
||||||
|
{{template "repo/issue/openclose" .}} |
||||||
|
</div> |
||||||
|
{{/* Ten wide does not cope well and makes the columns stack. |
||||||
|
This seems to be related to jQuery's hide/show: in fact, switching |
||||||
|
issue-actions and issue-filters and having this ten wide will show |
||||||
|
this one correctly, but not the other one. */}} |
||||||
|
<div class="nine wide right aligned right floated column"> |
||||||
|
<div class="ui secondary filter stackable menu"> |
||||||
|
{{if not .Repository.IsArchived}} |
||||||
|
<!-- Action Button --> |
||||||
|
{{if .IsShowClosed}} |
||||||
|
<div class="ui green active basic button issue-action" data-action="open" data-url="{{$.RepoLink}}/issues/status" style="margin-left: auto">{{.i18n.Tr "repo.issues.action_open"}}</div> |
||||||
|
{{else}} |
||||||
|
<div class="ui red active basic button issue-action" data-action="close" data-url="{{$.RepoLink}}/issues/status" style="margin-left: auto">{{.i18n.Tr "repo.issues.action_close"}}</div> |
||||||
|
{{end}} |
||||||
|
<!-- Labels --> |
||||||
|
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.action_label"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
{{range .Labels}} |
||||||
|
<div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels"> |
||||||
|
{{if contain $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}}<span class="label color" style="background-color: {{.Color}}"></span> {{.Name | RenderEmoji}} |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Milestone --> |
||||||
|
<div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.action_milestone"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/milestone"> |
||||||
|
{{.i18n.Tr "repo.issues.action_milestone_no_select"}} |
||||||
|
</div> |
||||||
|
{{range .Milestones}} |
||||||
|
<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/milestone"> |
||||||
|
{{.Name}} |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Projects --> |
||||||
|
<div class="ui {{if not .Projects}}disabled{{end}} dropdown jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.project_board"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/projects"> |
||||||
|
{{.i18n.Tr "repo.issues.new.no_projects"}} |
||||||
|
</div> |
||||||
|
{{range .Projects}} |
||||||
|
<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects"> |
||||||
|
{{.Title}} |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Assignees --> |
||||||
|
<div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item"> |
||||||
|
<span class="text"> |
||||||
|
{{.i18n.Tr "repo.issues.action_assignee"}} |
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}} |
||||||
|
</span> |
||||||
|
<div class="menu"> |
||||||
|
<div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/assignee"> |
||||||
|
{{.i18n.Tr "repo.issues.action_assignee_no_select"}} |
||||||
|
</div> |
||||||
|
{{range .Assignees}} |
||||||
|
<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/assignee"> |
||||||
|
{{avatar .}} {{.GetDisplayName}} |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div hidden> |
||||||
|
{{template "shared/issuelistfortree" mergeinto . "listType" "repo"}} |
||||||
|
<div id="repo_title" class="comment"></div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<script> |
||||||
|
var source = document.getElementById("repo-title-in-header"), |
||||||
|
destination = document.getElementById("repo_title"); |
||||||
|
|
||||||
|
destination.innerHTML = source.innerHTML |
||||||
|
|
||||||
|
var task_post = []; |
||||||
|
|
||||||
|
var config = { |
||||||
|
container: "#TaskChart", |
||||||
|
levelSeparation: 60, |
||||||
|
siblingSeparation: 60, |
||||||
|
nodeAlign: "BOTTOM", |
||||||
|
connectors: { |
||||||
|
type: "step", |
||||||
|
style: { |
||||||
|
"stroke-width": 4, |
||||||
|
"stroke": "#ccc", |
||||||
|
"stroke-dasharray": "-", //"", "-", ".", "-.", "-..", ". ", "- ", "--", "- .", "--.", "--.." |
||||||
|
"arrow-end": "classic-wide-long" |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
task_post0 = { |
||||||
|
innerHTML: "#repo_title" |
||||||
|
}; |
||||||
|
|
||||||
|
{{range .Issues}} |
||||||
|
task_post[{{.ID}}] = { |
||||||
|
parent: task_post0, |
||||||
|
innerHTML: "#task{{.ID}}" |
||||||
|
}, |
||||||
|
{{end}} |
||||||
|
|
||||||
|
{{range .Issues}} |
||||||
|
{{if .BlockedByParents}} |
||||||
|
task_post[{{.ID}}].parent = |
||||||
|
{{range .BlockedByParents}} |
||||||
|
task_post[{{.Issue.ID}}]; |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
|
||||||
|
|
||||||
|
tree_structure = [ |
||||||
|
config, task_post0, |
||||||
|
{{range .Issues}} |
||||||
|
task_post[{{.ID}}], |
||||||
|
{{end}} |
||||||
|
]; |
||||||
|
</script> |
||||||
|
<div class="chart Treant loaded" id="TaskChart"></div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.chart { |
||||||
|
height: 600px; |
||||||
|
width: 100%; |
||||||
|
border: 3px solid #DDD; |
||||||
|
border-radius: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
.comment { |
||||||
|
display: inline-block; |
||||||
|
width: 360px; |
||||||
|
} |
||||||
|
|
||||||
|
.comment { padding: 3px; border: 1px solid #ddd; border-radius: 3px; } |
||||||
|
</style> |
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://fperucic.github.io/treant-js/Treant.css"> |
||||||
|
<script src="https://fperucic.github.io/treant-js/vendor/raphael.js"></script> |
||||||
|
<script src="https://fperucic.github.io/treant-js/Treant.js"></script> |
||||||
|
<script> |
||||||
|
new Treant( tree_structure ); |
||||||
|
</script> |
||||||
|
|
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,144 @@ |
|||||||
|
<div class="issue list"> |
||||||
|
{{ $approvalCounts := .ApprovalCounts}} |
||||||
|
{{range .Issues}} |
||||||
|
<div id="task{{.ID}}" class="issue list comment"> |
||||||
|
<li class="item df py-3"> |
||||||
|
<div class="issue-item-left df"> |
||||||
|
{{if $.CanWriteIssuesOrPulls}} |
||||||
|
<div class="ui checkbox issue-checkbox"> |
||||||
|
<input type="checkbox" data-issue-id={{.ID}}></input> |
||||||
|
<label></label> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
<div class="issue-item-icon"> |
||||||
|
{{if .IsPull}} |
||||||
|
{{if .PullRequest.HasMerged}} |
||||||
|
{{svg "octicon-git-merge" 16 "text purple"}} |
||||||
|
{{else}} |
||||||
|
{{if .IsClosed}} |
||||||
|
{{svg "octicon-git-pull-request" 16 "text red"}} |
||||||
|
{{else}} |
||||||
|
{{svg "octicon-git-pull-request" 16 "text green"}} |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
{{else}} |
||||||
|
{{if .IsClosed}} |
||||||
|
{{svg "octicon-issue-closed" 16 "text red"}} |
||||||
|
{{else}} |
||||||
|
{{svg "octicon-issue-opened" 16 "text green"}} |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="issue-item-main f1 fc df"> |
||||||
|
<div class="issue-item-top-row"> |
||||||
|
<a class="title tdn" href="{{if .HTMLURL}}{{.HTMLURL}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji .Title}}</a> |
||||||
|
{{if .IsPull}} |
||||||
|
{{if (index $.CommitStatus .PullRequest.ID)}} |
||||||
|
{{template "repo/commit_status" (index $.CommitStatus .PullRequest.ID)}} |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
<span class="labels-list ml-2"> |
||||||
|
{{range .Labels}} |
||||||
|
<a class="ui label" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description | RenderEmojiPlain}}">{{.Name | RenderEmoji}}</a> |
||||||
|
{{end}} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="desc issue-item-bottom-row df ac fw my-1"> |
||||||
|
<a class="index ml-0 mr-2" href="{{if .HTMLURL}}{{.HTMLURL}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> |
||||||
|
{{if eq $.listType "dashboard"}} |
||||||
|
{{.Repo.FullName}}#{{.Index}} |
||||||
|
{{else}} |
||||||
|
#{{.Index}} |
||||||
|
{{end}} |
||||||
|
</a> |
||||||
|
{{ $timeStr := TimeSinceUnix .GetLastEventTimestamp $.Lang }} |
||||||
|
{{if .OriginalAuthor }} |
||||||
|
{{$.i18n.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} |
||||||
|
{{else if gt .Poster.ID 0}} |
||||||
|
{{$.i18n.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} |
||||||
|
{{else}} |
||||||
|
{{$.i18n.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} |
||||||
|
{{end}} |
||||||
|
{{if and .Milestone (ne $.listType "milestone")}} |
||||||
|
<a class="milestone" {{if $.RepoLink}}href="{{$.RepoLink}}/milestone/{{.Milestone.ID}}"{{else}}href="{{.Repo.Link}}/milestone/{{.Milestone.ID}}"{{end}}> |
||||||
|
{{svg "octicon-milestone" 14 "mr-2"}}{{.Milestone.Name}} |
||||||
|
</a> |
||||||
|
{{end}} |
||||||
|
{{if .Ref}} |
||||||
|
<a class="ref" {{if $.RepoLink}}href="{{index $.IssueRefURLs .ID}}"{{else}}href="{{.Repo.Link}}{{index $.IssueRefURLs .ID}}"{{end}}> |
||||||
|
{{svg "octicon-git-branch" 14 "mr-2"}}{{index $.IssueRefEndNames .ID}} |
||||||
|
</a> |
||||||
|
{{end}} |
||||||
|
{{$tasks := .GetTasks}} |
||||||
|
{{if gt $tasks 0}} |
||||||
|
{{$tasksDone := .GetTasksDone}} |
||||||
|
<span class="checklist"> |
||||||
|
{{svg "octicon-checklist" 14 "mr-2"}}{{$tasksDone}} / {{$tasks}} <span class="progress-bar"><span class="progress" style="width:calc(100% * {{$tasksDone}} / {{$tasks}});"></span></span> |
||||||
|
</span> |
||||||
|
{{end}} |
||||||
|
{{if ne .DeadlineUnix 0}} |
||||||
|
<span class="due-date tooltip" data-content="{{$.i18n.Tr "repo.issues.due_date"}}" data-position="right center"> |
||||||
|
<span{{if .IsOverdue}} class="overdue"{{end}}> |
||||||
|
{{svg "octicon-calendar" 14 "mr-2"}} |
||||||
|
{{.DeadlineUnix.FormatShort}} |
||||||
|
</span> |
||||||
|
</span> |
||||||
|
{{end}} |
||||||
|
{{if .IsPull}} |
||||||
|
{{$approveOfficial := call $approvalCounts .ID "approve"}} |
||||||
|
{{$rejectOfficial := call $approvalCounts .ID "reject"}} |
||||||
|
{{$waitingOfficial := call $approvalCounts .ID "waiting"}} |
||||||
|
{{if gt $approveOfficial 0}} |
||||||
|
<span class="approvals df ac"> |
||||||
|
{{svg "octicon-check" 14 "mr-1"}} |
||||||
|
{{$.i18n.TrN $approveOfficial "repo.pulls.approve_count_1" "repo.pulls.approve_count_n" $approveOfficial}} |
||||||
|
</span> |
||||||
|
{{end}} |
||||||
|
{{if gt $rejectOfficial 0}} |
||||||
|
<span class="rejects df ac"> |
||||||
|
{{svg "octicon-diff" 14 "mr-2"}} |
||||||
|
{{$.i18n.TrN $rejectOfficial "repo.pulls.reject_count_1" "repo.pulls.reject_count_n" $rejectOfficial}} |
||||||
|
</span> |
||||||
|
{{end}} |
||||||
|
{{if gt $waitingOfficial 0}} |
||||||
|
<span class="waiting df ac"> |
||||||
|
{{svg "octicon-eye" 14 "mr-2"}} |
||||||
|
{{$.i18n.TrN $waitingOfficial "repo.pulls.waiting_count_1" "repo.pulls.waiting_count_n" $waitingOfficial}} |
||||||
|
</span> |
||||||
|
{{end}} |
||||||
|
{{if and (not .PullRequest.HasMerged) (gt (len .PullRequest.ConflictedFiles) 0)}} |
||||||
|
<span class="conflicting df ac"> |
||||||
|
{{svg "octicon-x" 14}} |
||||||
|
{{$.i18n.TrN (len .PullRequest.ConflictedFiles) "repo.pulls.num_conflicting_files_1" "repo.pulls.num_conflicting_files_n" (len .PullRequest.ConflictedFiles)}} |
||||||
|
</span> |
||||||
|
{{end}} |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="issue-item-icons-right df p-2"> |
||||||
|
<div class="issue-item-icon-right text grey"> |
||||||
|
{{if .TotalTrackedTime}} |
||||||
|
{{svg "octicon-clock" 16 "mr-2"}} |
||||||
|
{{.TotalTrackedTime | Sec2Time}} |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
<div class="issue-item-icon-right text grey"> |
||||||
|
{{range .Assignees}} |
||||||
|
<a class="ui assignee tooltip tdn" href="{{.HomeLink}}" data-content="{{.GetDisplayName}}" data-position="left center"> |
||||||
|
{{avatar .}} |
||||||
|
</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
<div class="issue-item-icon-right text grey"> |
||||||
|
{{if .NumComments}} |
||||||
|
<a class="tdn" href="{{if .HTMLURL}}{{.HTMLURL}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> |
||||||
|
{{svg "octicon-comment" 16 "mr-2"}}{{.NumComments}} |
||||||
|
</a> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
</div> |
Loading…
Reference in new issue