sub_task #28

Merged
Bezborodov merged 2 commits from sub_task into dev_mirocod 3 years ago
  1. 7
      custom/conf/app.example.ini
  2. 2
      integrations/api_repo_edit_test.go
  3. 85
      models/error.go
  4. 52
      models/issue.go
  5. 38
      models/issue_comment.go
  6. 134
      models/issue_parent.go
  7. 61
      models/issue_parent_test.go
  8. 3
      models/migrations/v70.go
  9. 1
      models/repo.go
  10. 16
      models/repo/issue.go
  11. 1
      models/repo/repo_unit.go
  12. 5
      modules/context/repo.go
  13. 1
      modules/convert/repository.go
  14. 4
      modules/doctor/fix16961.go
  15. 4
      modules/doctor/fix16961_test.go
  16. 4
      modules/setting/service.go
  17. 2
      modules/structs/repo.go
  18. 30
      options/locale/locale_ru-RU.ini
  19. 2
      routers/api/v1/repo/repo.go
  20. 71
      routers/web/repo/issue.go
  21. 129
      routers/web/repo/issue_parent.go
  22. 2
      routers/web/repo/milestone.go
  23. 1
      routers/web/repo/setting.go
  24. 5
      routers/web/web.go
  25. 1
      services/forms/repo_form.go
  26. 2
      templates/admin/config.tmpl
  27. 2
      templates/repo/header.tmpl
  28. 1
      templates/repo/issue/navbar.tmpl
  29. 293
      templates/repo/issue/tree.tmpl
  30. 125
      templates/repo/issue/view_content/sidebar.tmpl
  31. 6
      templates/repo/settings/options.tmpl
  32. 144
      templates/shared/issuelistfortree.tmpl
  33. 5
      templates/swagger/v1_json.tmpl
  34. 46
      web_src/js/features/repo-issue.js
  35. 3
      web_src/js/features/repo-legacy.js
  36. 15
      web_src/less/_repository.less

7
custom/conf/app.example.ini

@ -701,6 +701,13 @@ PATH =
;; Dependencies can be added from any repository where the user is granted access or only from the current repository depending on this setting. ;; Dependencies can be added from any repository where the user is granted access or only from the current repository depending on this setting.
;ALLOW_CROSS_REPOSITORY_DEPENDENCIES = true ;ALLOW_CROSS_REPOSITORY_DEPENDENCIES = true
;; ;;
;; Default value for EnableParents
;; Repositories will use parents by default depending on this setting
;DEFAULT_ENABLE_PARENTS = true
;;
;; Parents can be added from any repository where the user is granted access or only from the current repository depending on this setting.
;ALLOW_CROSS_REPOSITORY_PARENTS = true
;;
;; Enable heatmap on users profiles. ;; Enable heatmap on users profiles.
;ENABLE_USER_HEATMAP = true ;ENABLE_USER_HEATMAP = true
;; ;;

2
integrations/api_repo_edit_test.go

@ -35,6 +35,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
EnableTimeTracker: config.EnableTimetracker, EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies, EnableIssueDependencies: config.EnableDependencies,
EnableIssueParents: config.EnableParents,
} }
} else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil { } else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig() config := unit.ExternalTrackerConfig()
@ -182,6 +183,7 @@ func TestAPIRepoEdit(t *testing.T) {
EnableTimeTracker: false, EnableTimeTracker: false,
AllowOnlyContributorsToTrackTime: false, AllowOnlyContributorsToTrackTime: false,
EnableIssueDependencies: false, EnableIssueDependencies: false,
EnableIssueParents: false,
} }
*repoEditOption.HasWiki = true *repoEditOption.HasWiki = true
repoEditOption.ExternalWiki = nil repoEditOption.ExternalWiki = nil

85
models/error.go

@ -1340,6 +1340,91 @@ func (err ErrUnknownDependencyType) Error() string {
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
} }
// .___ ________ .___ .__
// | | ______ ________ __ ____ \______ \ ____ ______ ____ ____ __| _/____ ____ ____ |__| ____ ______
// | |/ ___// ___/ | \_/ __ \ | | \_/ __ \\____ \_/ __ \ / \ / __ |/ __ \ / \_/ ___\| |/ __ \ / ___/
// | |\___ \ \___ \| | /\ ___/ | ` \ ___/| |_> > ___/| | \/ /_/ \ ___/| | \ \___| \ ___/ \___ \
// |___/____ >____ >____/ \___ >_______ /\___ > __/ \___ >___| /\____ |\___ >___| /\___ >__|\___ >____ >
// \/ \/ \/ \/ \/|__| \/ \/ \/ \/ \/ \/ \/ \/
// ErrParentExists represents a "ParentAlreadyExists" kind of error.
type ErrParentExists struct {
IssueID int64
ParentID int64
}
// IsErrParentExists checks if an error is a ErrParentExists.
func IsErrParentExists(err error) bool {
_, ok := err.(ErrParentExists)
return ok
}
func (err ErrParentExists) Error() string {
return fmt.Sprintf("issue parent does already exist [issue id: %d, parent id: %d]", err.IssueID, err.ParentID)
}
// ErrParentNotExists represents a "ParentAlreadyExists" kind of error.
type ErrParentNotExists struct {
IssueID int64
ParentID int64
}
// IsErrParentNotExists checks if an error is a ErrParentExists.
func IsErrParentNotExists(err error) bool {
_, ok := err.(ErrParentNotExists)
return ok
}
func (err ErrParentNotExists) Error() string {
return fmt.Sprintf("issue parent does not exist [issue id: %d, parent id: %d]", err.IssueID, err.ParentID)
}
// ErrCircularParent represents a "ParentCircular" kind of error.
type ErrCircularParent struct {
IssueID int64
ParentID int64
}
// IsErrCircularParent checks if an error is a ErrCircularParent.
func IsErrCircularParent(err error) bool {
_, ok := err.(ErrCircularParent)
return ok
}
func (err ErrCircularParent) Error() string {
return fmt.Sprintf("circular parents exists (two issues blocking each other) [issue id: %d, parent id: %d]", err.IssueID, err.ParentID)
}
// ErrParentsLeft represents an error where the issue you're trying to close still has parents left.
type ErrParentsLeft struct {
IssueID int64
}
// IsErrParentsLeft checks if an error is a ErrParentsLeft.
func IsErrParentsLeft(err error) bool {
_, ok := err.(ErrParentsLeft)
return ok
}
func (err ErrParentsLeft) Error() string {
return fmt.Sprintf("issue has open parents [issue id: %d]", err.IssueID)
}
// ErrUnknownParentType represents an error where an unknown parent type was passed
type ErrUnknownParentType struct {
Type ParentType
}
// IsErrUnknownParentType checks if an error is ErrUnknownParentType
func IsErrUnknownParentType(err error) bool {
_, ok := err.(ErrUnknownParentType)
return ok
}
func (err ErrUnknownParentType) Error() string {
return fmt.Sprintf("unknown parent type [type: %d]", err.Type)
}
// __________ .__ // __________ .__
// \______ \ _______ _|__| ______ _ __ // \______ \ _______ _|__| ______ _ __
// | _// __ \ \/ / |/ __ \ \/ \/ / // | _// __ \ \/ / |/ __ \ \/ \/ /

52
models/issue.go

@ -1990,6 +1990,12 @@ type DependencyInfo struct {
repo_model.Repository `xorm:"extends"` repo_model.Repository `xorm:"extends"`
} }
// ParentInfo represents high level information about an issue which is a parent of another issue.
type ParentInfo struct {
Issue `xorm:"extends"`
repo_model.Repository `xorm:"extends"`
}
// getParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author // getParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
func (issue *Issue) getParticipantIDsByIssue(e db.Engine) ([]int64, error) { func (issue *Issue) getParticipantIDsByIssue(e db.Engine) ([]int64, error) {
if issue == nil { if issue == nil {
@ -2048,6 +2054,42 @@ func (issue *Issue) getBlockingDependencies(e db.Engine) (issueDeps []*Dependenc
return issueDeps, err return issueDeps, err
} }
// Get Blocked By Parents, aka all issues this issue is blocked by.
func (issue *Issue) getBlockedByParents(e db.Engine) (issueParents []*ParentInfo, err error) {
err = e.
Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_parent", "issue_parent.parent_id = issue.id").
Where("issue_id = ?", issue.ID).
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(issue.RepoID, 10) + " THEN 0 ELSE issue.repo_id END, issue.created_unix DESC").
Find(&issueParents)
for _, parentInfo := range issueParents {
parentInfo.Issue.Repo = &parentInfo.Repository
}
return issueParents, err
}
// Get Blocking Parents, aka all issues this issue blocks.
func (issue *Issue) getBlockingParents(e db.Engine) (issueParents []*ParentInfo, err error) {
err = e.
Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_parent", "issue_parent.issue_id = issue.id").
Where("parent_id = ?", issue.ID).
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(issue.RepoID, 10) + " THEN 0 ELSE issue.repo_id END, issue.created_unix DESC").
Find(&issueParents)
for _, parentInfo := range issueParents {
parentInfo.Issue.Repo = &parentInfo.Repository
}
return issueParents, err
}
// BlockedByDependencies finds all Dependencies an issue is blocked by // BlockedByDependencies finds all Dependencies an issue is blocked by
func (issue *Issue) BlockedByDependencies() ([]*DependencyInfo, error) { func (issue *Issue) BlockedByDependencies() ([]*DependencyInfo, error) {
return issue.getBlockedByDependencies(db.GetEngine(db.DefaultContext)) return issue.getBlockedByDependencies(db.GetEngine(db.DefaultContext))
@ -2058,6 +2100,16 @@ func (issue *Issue) BlockingDependencies() ([]*DependencyInfo, error) {
return issue.getBlockingDependencies(db.GetEngine(db.DefaultContext)) return issue.getBlockingDependencies(db.GetEngine(db.DefaultContext))
} }
// BlockedByParents finds all Parents an issue is blocked by
func (issue *Issue) BlockedByParents() ([]*ParentInfo, error) {
return issue.getBlockedByParents(db.GetEngine(db.DefaultContext))
}
// BlockingParents returns all blocking dependencies, aka all other issues a given issue blocks
func (issue *Issue) BlockingParents() ([]*ParentInfo, error) {
return issue.getBlockingParents(db.GetEngine(db.DefaultContext))
}
func (issue *Issue) updateClosedNum(ctx context.Context) (err error) { func (issue *Issue) updateClosedNum(ctx context.Context) (err error) {
if issue.IsPull { if issue.IsPull {
err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls") err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls")

38
models/issue_comment.go

@ -82,6 +82,10 @@ const (
CommentTypeAddDependency CommentTypeAddDependency
// 20 Dependency removed // 20 Dependency removed
CommentTypeRemoveDependency CommentTypeRemoveDependency
// 19 Parent added
CommentTypeAddParent
// 20 Parent removed
CommentTypeRemoveParent
// 21 Comment a line of code // 21 Comment a line of code
CommentTypeCode CommentTypeCode
// 22 Reviews a pull request by giving general feedback // 22 Reviews a pull request by giving general feedback
@ -933,6 +937,39 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is
return return
} }
// Creates issue parent comment
func createIssueParentComment(ctx context.Context, doer *user_model.User, issue, parentIssue *Issue, add bool) (err error) {
cType := CommentTypeAddParent
if !add {
cType = CommentTypeRemoveParent
}
if err = issue.loadRepo(ctx); err != nil {
return
}
// Make two comments, one in each issue
opts := &CreateCommentOptions{
Type: cType,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
ParentIssueID: parentIssue.ID,
}
if _, err = createComment(ctx, opts); err != nil {
return
}
opts = &CreateCommentOptions{
Type: cType,
Doer: doer,
Repo: issue.Repo,
Issue: parentIssue,
ParentIssueID: issue.ID,
}
_, err = createComment(ctx, opts)
return
}
// CreateCommentOptions defines options for creating comment // CreateCommentOptions defines options for creating comment
type CreateCommentOptions struct { type CreateCommentOptions struct {
Type CommentType Type CommentType
@ -942,6 +979,7 @@ type CreateCommentOptions struct {
Label *Label Label *Label
DependentIssueID int64 DependentIssueID int64
ParentIssueID int64
OldMilestoneID int64 OldMilestoneID int64
MilestoneID int64 MilestoneID int64
OldProjectID int64 OldProjectID int64

134
models/issue_parent.go

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

61
models/issue_parent_test.go

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

3
models/migrations/v70.go

@ -102,6 +102,9 @@ func addIssueDependencies(x *xorm.Engine) (err error) {
if _, ok := unit.Config["EnableDependencies"]; !ok { if _, ok := unit.Config["EnableDependencies"]; !ok {
unit.Config["EnableDependencies"] = setting.Service.DefaultEnableDependencies unit.Config["EnableDependencies"] = setting.Service.DefaultEnableDependencies
} }
if _, ok := unit.Config["EnableParents"]; !ok {
unit.Config["EnableParents"] = setting.Service.DefaultEnableParents
}
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil { if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil {
return err return err
} }

1
models/repo.go

@ -515,6 +515,7 @@ func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_
EnableTimetracker: setting.Service.DefaultEnableTimetracking, EnableTimetracker: setting.Service.DefaultEnableTimetracking,
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
EnableDependencies: setting.Service.DefaultEnableDependencies, EnableDependencies: setting.Service.DefaultEnableDependencies,
EnableParents: setting.Service.DefaultEnableParents,
}, },
}) })
} else if tp == unit.TypePullRequests { } else if tp == unit.TypePullRequests {

16
models/repo/issue.go

@ -70,3 +70,19 @@ func (repo *Repository) IsDependenciesEnabledCtx(ctx context.Context) bool {
} }
return u.IssuesConfig().EnableDependencies return u.IssuesConfig().EnableDependencies
} }
// IsParentsEnabled returns if parents are enabled and returns the default setting if not set.
func (repo *Repository) IsParentsEnabled() bool {
return repo.IsParentsEnabledCtx(db.DefaultContext)
}
// IsParentsEnabledCtx returns if parents are enabled and returns the default setting if not set.
func (repo *Repository) IsParentsEnabledCtx(ctx context.Context) bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnitCtx(ctx, unit.TypeIssues); err != nil {
log.Trace("%s", err)
return setting.Service.DefaultEnableParents
}
return u.IssuesConfig().EnableParents
}

1
models/repo/repo_unit.go

@ -94,6 +94,7 @@ type IssuesConfig struct {
EnableTimetracker bool EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool AllowOnlyContributorsToTrackTime bool
EnableDependencies bool EnableDependencies bool
EnableParents bool
} }
// FromDB fills up a IssuesConfig from serialized format. // FromDB fills up a IssuesConfig from serialized format.

5
modules/context/repo.go

@ -164,6 +164,11 @@ func (r *Repository) CanCreateIssueDependencies(user *user_model.User, isPull bo
return r.Repository.IsDependenciesEnabled() && r.Permission.CanWriteIssuesOrPulls(isPull) return r.Repository.IsDependenciesEnabled() && r.Permission.CanWriteIssuesOrPulls(isPull)
} }
// CanCreateIssueParents returns whether or not a user can create parents.
func (r *Repository) CanCreateIssueParents(user *user_model.User, isPull bool) bool {
return r.Repository.IsParentsEnabled() && r.Permission.CanWriteIssuesOrPulls(isPull)
}
// GetCommitsCount returns cached commit count for current view // GetCommitsCount returns cached commit count for current view
func (r *Repository) GetCommitsCount() (int64, error) { func (r *Repository) GetCommitsCount() (int64, error) {
var contextName string var contextName string

1
modules/convert/repository.go

@ -51,6 +51,7 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
EnableTimeTracker: config.EnableTimetracker, EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies, EnableIssueDependencies: config.EnableDependencies,
EnableIssueParents: config.EnableParents,
} }
} else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil { } else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig() config := unit.ExternalTrackerConfig()

4
modules/doctor/fix16961.go

@ -206,6 +206,10 @@ func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool,
if parseErr != nil { if parseErr != nil {
return return
} }
cfg.EnableParents, parseErr = parseBool16961(parts[3])
if parseErr != nil {
return
}
return true, nil return true, nil
} }

4
modules/doctor/fix16961_test.go

@ -237,11 +237,12 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
}{ }{
{ {
name: "normal", name: "normal",
bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true}`, bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true,"EnableParents":true}`,
expected: repo_model.IssuesConfig{ expected: repo_model.IssuesConfig{
EnableTimetracker: true, EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true, AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true, EnableDependencies: true,
EnableParents: true,
}, },
}, },
{ {
@ -251,6 +252,7 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
EnableTimetracker: true, EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true, AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true, EnableDependencies: true,
EnableParents: true,
}, },
wantFixed: true, wantFixed: true,
}, },

4
modules/setting/service.go

@ -53,7 +53,9 @@ var Service = struct {
EnableTimetracking bool EnableTimetracking bool
DefaultEnableTimetracking bool DefaultEnableTimetracking bool
DefaultEnableDependencies bool DefaultEnableDependencies bool
DefaultEnableParents bool
AllowCrossRepositoryDependencies bool AllowCrossRepositoryDependencies bool
AllowCrossRepositoryParents bool
DefaultAllowOnlyContributorsToTrackTime bool DefaultAllowOnlyContributorsToTrackTime bool
NoReplyAddress string NoReplyAddress string
EnableUserHeatmap bool EnableUserHeatmap bool
@ -141,7 +143,9 @@ func newService() {
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true) Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
} }
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true) Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
Service.DefaultEnableParents = sec.Key("DEFAULT_ENABLE_PARENTS").MustBool(true)
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true) Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
Service.AllowCrossRepositoryParents = sec.Key("ALLOW_CROSS_REPOSITORY_PARENTS").MustBool(true)
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true) Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + Domain) Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + Domain)
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)

2
modules/structs/repo.go

@ -25,6 +25,8 @@ type InternalTracker struct {
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"` AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
// Enable dependencies for issues and pull requests (Built-in issue tracker) // Enable dependencies for issues and pull requests (Built-in issue tracker)
EnableIssueDependencies bool `json:"enable_issue_dependencies"` EnableIssueDependencies bool `json:"enable_issue_dependencies"`
// Enable parents for issues and pull requests (Built-in issue tracker)
EnableIssueParents bool `json:"enable_issue_parents"`
} }
// ExternalTracker represents settings for external tracker // ExternalTracker represents settings for external tracker

30
options/locale/locale_ru-RU.ini

@ -991,6 +991,8 @@ file_view_raw=Посмотреть исходник
file_permalink=Постоянная ссылка file_permalink=Постоянная ссылка
file_too_large=Этот файл слишком большой, поэтому он не может быть отображён. file_too_large=Этот файл слишком большой, поэтому он не может быть отображён.
issues_tree=Дерево задач
file_copy_permalink=Копировать постоянную ссылку file_copy_permalink=Копировать постоянную ссылку
video_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'video' тэг. video_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'video' тэг.
audio_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'audio' тэг. audio_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'audio' тэг.
@ -1372,6 +1374,33 @@ issues.dependency.add_error_dep_not_exist=Зависимости не сущес
issues.dependency.add_error_dep_exists=Зависимость уже существует. issues.dependency.add_error_dep_exists=Зависимость уже существует.
issues.dependency.add_error_cannot_create_circular=Вы не можете создать зависимость с двумя задачами, блокирующими друг друга. issues.dependency.add_error_cannot_create_circular=Вы не можете создать зависимость с двумя задачами, блокирующими друг друга.
issues.dependency.add_error_dep_not_same_repo=Обе задачи должны находиться в одном репозитории. issues.dependency.add_error_dep_not_same_repo=Обе задачи должны находиться в одном репозитории.
issues.parent.title=Родительские задачи
issues.parent.issue_no_parents=В настоящее время эта задача не имеет родителей.
issues.parent.pr_no_parents=Этот запрос на слияние в настоящее время не имеет никаких родителей.
issues.parent.add=Добавить родителя…
issues.parent.cancel=Отменить
issues.parent.remove=Удалить
issues.parent.remove_info=Удалить этого родителя
issues.parent.added_parent=`добавить нового родителя %s`
issues.parent.removed_parent=`убрал родителя %s`
issues.parent.pr_closing_blockedby=Этот запрос на слияние имеет родителей
issues.parent.issue_closing_blockedby=Эта задача имеет родителей
issues.parent.issue_close_blocks=Эта задача имеет следующих детей
issues.parent.pr_close_blocks=Этот запрос на слияние имеет следующих детей
issues.parent.blocks_short=Дети
issues.parent.blocked_by_short=Родители
issues.parent.remove_header=Удалить родителя
issues.parent.issue_remove_text=Это приведет к удалению родителя от этой задачи. Продолжить?
issues.parent.pr_remove_text=Это приведёт к удалению родителя от этого запроса на слияние. Продолжить?
issues.parent.setting=Включение родителя для задач и запросов на слияние
issues.parent.add_error_same_issue=Вы не можете указать родителя задачи на саму себя.
issues.parent.add_error_dep_issue_not_exist=Родительсткая задача не существует.
issues.parent.add_error_dep_not_exist=Родительсткой задачи не существует.
issues.parent.add_error_dep_exists=Привязка к родителю уже существует.
issues.parent.add_error_cannot_create_circular=Вы не можете создать родителей с двумя задачами, блокирующими друг друга.
issues.parent.add_error_dep_not_same_repo=Обе задачи должны находиться в одном репозитории.
issues.review.self.approval=Вы не можете одобрить собственный запрос на слияние. issues.review.self.approval=Вы не можете одобрить собственный запрос на слияние.
issues.review.self.rejection=Невозможно запрашивать изменения своего запроса на слияние. issues.review.self.rejection=Невозможно запрашивать изменения своего запроса на слияние.
issues.review.approve=одобрил(а) эти изменения %s issues.review.approve=одобрил(а) эти изменения %s
@ -2623,6 +2652,7 @@ config.default_allow_only_contributors_to_track_time=Учитывать толь
config.no_reply_address=No-reply адрес config.no_reply_address=No-reply адрес
config.default_visibility_organization=Видимость по умолчанию для новых организаций config.default_visibility_organization=Видимость по умолчанию для новых организаций
config.default_enable_dependencies=Включение зависимостей для задач по умолчанию config.default_enable_dependencies=Включение зависимостей для задач по умолчанию
config.default_enable_parents=Включение родителей для задач по умолчанию
config.webhook_config=Конфигурация вебхуков config.webhook_config=Конфигурация вебхуков
config.queue_length=Длина очереди config.queue_length=Длина очереди

2
routers/api/v1/repo/repo.go

@ -776,6 +776,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: opts.InternalTracker.EnableTimeTracker, EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
EnableDependencies: opts.InternalTracker.EnableIssueDependencies, EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
EnableParents: opts.InternalTracker.EnableIssueParents,
} }
} else if unit, err := repo.GetUnit(unit_model.TypeIssues); err != nil { } else if unit, err := repo.GetUnit(unit_model.TypeIssues); err != nil {
// Unit type doesn't exist so we make a new config file with default values // Unit type doesn't exist so we make a new config file with default values
@ -783,6 +784,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: true, EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true, AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true, EnableDependencies: true,
EnableParents: true,
} }
} else { } else {
config = unit.IssuesConfig() config = unit.IssuesConfig()

71
routers/web/repo/issue.go

@ -47,6 +47,7 @@ const (
tplAttachment base.TplName = "repo/issue/view_content/attachments" tplAttachment base.TplName = "repo/issue/view_content/attachments"
tplIssues base.TplName = "repo/issue/list" tplIssues base.TplName = "repo/issue/list"
tplIssuesTree base.TplName = "repo/issue/tree"
tplIssueNew base.TplName = "repo/issue/new" tplIssueNew base.TplName = "repo/issue/new"
tplIssueChoose base.TplName = "repo/issue/choose" tplIssueChoose base.TplName = "repo/issue/choose"
tplIssueView base.TplName = "repo/issue/view" tplIssueView base.TplName = "repo/issue/view"
@ -114,7 +115,7 @@ func MustAllowPulls(ctx *context.Context) {
} }
} }
func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) { func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool, page_size int) {
var err error var err error
viewType := ctx.FormString("type") viewType := ctx.FormString("type")
sortType := ctx.FormString("sort") sortType := ctx.FormString("sort")
@ -210,7 +211,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
} else { } else {
total = int(issueStats.ClosedCount) total = int(issueStats.ClosedCount)
} }
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) pager := context.NewPagination(total, page_size, page, 5)
var mileIDs []int64 var mileIDs []int64
if milestoneID > 0 { if milestoneID > 0 {
@ -224,7 +225,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
issues, err = models.Issues(&models.IssuesOptions{ issues, err = models.Issues(&models.IssuesOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
Page: pager.Paginater.Current(), Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum, PageSize: page_size,
}, },
RepoID: repo.ID, RepoID: repo.ID,
AssigneeID: assigneeID, AssigneeID: assigneeID,
@ -391,7 +392,7 @@ func Issues(ctx *context.Context) {
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
} }
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList), setting.UI.IssuePagingNum)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -412,6 +413,47 @@ func Issues(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplIssues) ctx.HTML(http.StatusOK, tplIssues)
} }
// Issues Tree render issues page
func IssuesTree(ctx *context.Context) {
isPullList := ctx.Params(":type") == "pulls"
if isPullList {
MustAllowPulls(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctx.Tr("repo.pulls")
ctx.Data["PageIsPullList"] = true
} else {
MustEnableIssues(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctx.Tr("repo.issues_tree")
ctx.Data["PageIsIssuesTree"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
}
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList), 32*1024)
if ctx.Written() {
return
}
var err error
// Get milestones
ctx.Data["Milestones"], _, err = models.GetMilestones(models.GetMilestonesOption{
RepoID: ctx.Repo.Repository.ID,
State: api.StateType(ctx.FormString("state")),
})
if err != nil {
ctx.ServerError("GetAllRepoMilestones", err)
return
}
ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
ctx.HTML(http.StatusOK, tplIssuesTree)
}
// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) { func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
var err error var err error
@ -701,6 +743,9 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
// Contains true if the user can create issue dependencies // Contains true if the user can create issue dependencies
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull) ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
// Contains true if the user can create issue parents
ctx.Data["CanCreateIssueParents"] = ctx.Repo.CanCreateIssueParents(ctx.User, isPull)
return labels return labels
} }
@ -1324,6 +1369,12 @@ func ViewIssue(ctx *context.Context) {
// check if dependencies can be created across repositories // check if dependencies can be created across repositories
ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
// Check if the user can use the parents
ctx.Data["CanCreateIssueParents"] = ctx.Repo.CanCreateIssueParents(ctx.User, issue.IsPull)
// check if parents can be created across repositories
ctx.Data["AllowCrossRepositoryParents"] = setting.Service.AllowCrossRepositoryParents
if issue.ShowRole, err = roleDescriptor(repo, issue.Poster, issue); err != nil { if issue.ShowRole, err = roleDescriptor(repo, issue.Poster, issue); err != nil {
ctx.ServerError("roleDescriptor", err) ctx.ServerError("roleDescriptor", err)
return return
@ -1650,6 +1701,18 @@ func ViewIssue(ctx *context.Context) {
return return
} }
// Get Parents
ctx.Data["BlockedByParents"], err = issue.BlockedByParents()
if err != nil {
ctx.ServerError("BlockedByParents", err)
return
}
ctx.Data["BlockingParents"], err = issue.BlockingParents()
if err != nil {
ctx.ServerError("BlockingParents", err)
return
}
ctx.Data["Participants"] = participants ctx.Data["Participants"] = participants
ctx.Data["NumParticipants"] = len(participants) ctx.Data["NumParticipants"] = len(participants)
ctx.Data["Issue"] = issue ctx.Data["Issue"] = issue

129
routers/web/repo/issue_parent.go

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

2
routers/web/repo/milestone.go

@ -289,7 +289,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Title"] = milestone.Name ctx.Data["Title"] = milestone.Name
ctx.Data["Milestone"] = milestone ctx.Data["Milestone"] = milestone
issues(ctx, milestoneID, 0, util.OptionalBoolNone) issues(ctx, milestoneID, 0, util.OptionalBoolNone, setting.UI.IssuePagingNum)
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)

1
routers/web/repo/setting.go

@ -434,6 +434,7 @@ func SettingsPost(ctx *context.Context) {
EnableTimetracker: form.EnableTimetracker, EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies, EnableDependencies: form.EnableIssueDependencies,
EnableParents: form.EnableIssueParents,
}, },
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)

5
routers/web/web.go

@ -741,6 +741,10 @@ func RegisterRoutes(m *web.Route) {
m.Post("/add", repo.AddDependency) m.Post("/add", repo.AddDependency)
m.Post("/delete", repo.RemoveDependency) m.Post("/delete", repo.RemoveDependency)
}) })
m.Group("/parent", func() {
m.Post("/add", repo.AddParent)
m.Post("/delete", repo.RemoveParent)
})
m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment) m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
m.Group("/times", func() { m.Group("/times", func() {
m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually) m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
@ -881,6 +885,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Group("", func() { m.Group("", func() {
m.Get("/{type:issues|pulls}", repo.Issues) m.Get("/{type:issues|pulls}", repo.Issues)
m.Get("/issues_tree", repo.IssuesTree)
m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue) m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue)
m.Group("/{type:issues|pulls}/{index}/content-history", func() { m.Group("/{type:issues|pulls}/{index}/content-history", func() {
m.Get("/overview", repo.GetContentHistoryOverview) m.Get("/overview", repo.GetContentHistoryOverview)

1
services/forms/repo_form.go

@ -155,6 +155,7 @@ type RepoSettingForm struct {
EnableTimetracker bool EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool EnableIssueDependencies bool
EnableIssueParents bool
IsArchived bool IsArchived bool
// Signing Settings // Signing Settings

2
templates/admin/config.tmpl

@ -186,6 +186,8 @@
<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd> <dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
<dt>{{.i18n.Tr "admin.config.default_enable_dependencies"}}</dt> <dt>{{.i18n.Tr "admin.config.default_enable_dependencies"}}</dt>
<dd>{{if .Service.DefaultEnableDependencies}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd> <dd>{{if .Service.DefaultEnableDependencies}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
<dt>{{.i18n.Tr "admin.config.default_enable_parents"}}</dt>
<dd>{{if .Service.DefaultEnableParents}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
<div class="ui divider"></div> <div class="ui divider"></div>
<dt>{{.i18n.Tr "admin.config.active_code_lives"}}</dt> <dt>{{.i18n.Tr "admin.config.active_code_lives"}}</dt>
<dd>{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}</dd> <dd>{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}</dd>

2
templates/repo/header.tmpl

@ -2,7 +2,7 @@
{{with .Repository}} {{with .Repository}}
<div class="ui container"> <div class="ui container">
<div class="repo-header"> <div class="repo-header">
<div class="repo-title-wrap df fc"> <div id="repo-title-in-header" class="repo-title-wrap df fc">
<div class="repo-title"> <div class="repo-title">
{{$avatar := (repoAvatar . 32 "mr-3")}} {{$avatar := (repoAvatar . 32 "mr-3")}}
{{if $avatar}} {{if $avatar}}

1
templates/repo/issue/navbar.tmpl

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

293
templates/repo/issue/tree.tmpl

@ -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" .}}

125
templates/repo/issue/view_content/sidebar.tmpl

@ -444,6 +444,131 @@
{{end}} {{end}}
</div> </div>
{{if .Repository.IsParentsEnabled}}
<div class="ui divider"></div>
<div class="ui depending">
{{if (and (not .BlockedByParents) (not .BlockingParents))}}
<span class="text"><strong>{{.i18n.Tr "repo.issues.parent.title"}}</strong></span>
<br>
<p>
{{if .Issue.IsPull}}
{{.i18n.Tr "repo.issues.parent.pr_no_parents"}}
{{else}}
{{.i18n.Tr "repo.issues.parent.issue_no_parents"}}
{{end}}
</p>
{{end}}
{{if .BlockingParents}}
<span class="text tooltip" data-content="{{if .Issue.IsPull}}{{.i18n.Tr "repo.issues.parent.pr_close_blocks"}}{{else}}{{.i18n.Tr "repo.issues.parent.issue_close_blocks"}}{{end}}">
<strong>{{.i18n.Tr "repo.issues.parent.blocks_short"}}</strong>
</span>
<div class="ui relaxed divided list">
{{range .BlockingParents}}
<div class="item parent{{if .Issue.IsClosed}} is-closed{{end}} df ac sb">
<div class="item-left df jc fc f1">
<a class="title" href="{{.Issue.Link}}">
#{{.Issue.Index}} {{.Issue.Title | RenderEmoji}}
</a>
<div class="text small">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right df ac">
{{if and $.CanCreateIssueParents (not $.Repository.IsArchived)}}
<a class="delete-parent-button tooltip ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-content="{{$.i18n.Tr "repo.issues.parent.remove_info"}}" data-inverted="">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
{{if .BlockedByParents}}
<span class="text tooltip" data-content="{{if .Issue.IsPull}}{{.i18n.Tr "repo.issues.parent.pr_closing_blockedby"}}{{else}}{{.i18n.Tr "repo.issues.parent.issue_closing_blockedby"}}{{end}}">
<strong>{{.i18n.Tr "repo.issues.parent.blocked_by_short"}}</strong>
</span>
<div class="ui relaxed divided list">
{{range .BlockedByParents}}
<div class="item parent{{if .Issue.IsClosed}} is-closed{{end}} df ac sb">
<div class="item-left df jc fc f1">
<a class="title" href="{{.Issue.Link}}">
#{{.Issue.Index}} {{.Issue.Title | RenderEmoji}}
</a>
<div class="text small">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right df ac">
{{if and $.CanCreateIssueParents (not $.Repository.IsArchived)}}
<a class="delete-parent-button tooltip ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-content="{{$.i18n.Tr "repo.issues.parent.remove_info"}}" data-inverted="">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
{{if and .CanCreateIssueParents (not .Repository.IsArchived)}}
<div>
<form method="POST" action="{{.Issue.Link}}/parent/add" id="addParentForm">
{{$.CsrfTokenHtml}}
<div class="ui fluid action input">
<div class="ui search selection dropdown" id="new-parent-drop-list" data-issue-id="{{.Issue.ID}}">
<input name="newParent" type="hidden">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<input type="text" class="search">
<div class="default text">{{.i18n.Tr "repo.issues.parent.add"}}</div>
</div>
<button class="ui green icon button">
{{svg "octicon-plus"}}
</button>
</div>
</form>
</div>
{{end}}
</div>
{{if and .CanCreateIssueParents (not .Repository.IsArchived)}}
<input type="hidden" id="crossRepoSearch" value="{{.AllowCrossRepositoryParents}}">
<div class="ui basic modal remove-parent">
<div class="ui icon header">
{{svg "octicon-trash"}}
{{.i18n.Tr "repo.issues.parent.remove_header"}}
</div>
<div class="content">
<form method="POST" action="{{.Issue.Link}}/parent/delete" id="removeParentForm">
{{$.CsrfTokenHtml}}
<input type="hidden" value="" name="removeParentID" id="removeParentID"/>
<input type="hidden" value="" name="parentType" id="parentType"/>
</form>
<p>{{if .Issue.IsPull}}
{{.i18n.Tr "repo.issues.parent.pr_remove_text"}}
{{else}}
{{.i18n.Tr "repo.issues.parent.issue_remove_text"}}
{{end}}</p>
</div>
<div class="actions">
<div class="ui red cancel inverted button">
{{svg "octicon-x"}}
{{.i18n.Tr "repo.issues.parent.cancel"}}
</div>
<div class="ui green ok inverted button">
{{svg "octicon-check"}}
{{.i18n.Tr "repo.issues.parent.remove"}}
</div>
</div>
</div>
{{end}}
{{end}}
{{if .Repository.IsDependenciesEnabled}} {{if .Repository.IsDependenciesEnabled}}
<div class="ui divider"></div> <div class="ui divider"></div>

6
templates/repo/settings/options.tmpl

@ -329,6 +329,12 @@
<label>{{.i18n.Tr "repo.issues.dependency.setting"}}</label> <label>{{.i18n.Tr "repo.issues.dependency.setting"}}</label>
</div> </div>
</div> </div>
<div class="field">
<div class="ui checkbox">
<input name="enable_issue_parents" type="checkbox" {{if (.Repository.IsParentsEnabled)}}checked{{end}}>
<label>{{.i18n.Tr "repo.issues.parent.setting"}}</label>
</div>
</div>
<div class="ui checkbox"> <div class="ui checkbox">
<input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{ if .Repository.CloseIssuesViaCommitInAnyBranch }}checked{{end}}> <input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{ if .Repository.CloseIssuesViaCommitInAnyBranch }}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label> <label>{{.i18n.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>

144
templates/shared/issuelistfortree.tmpl

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

5
templates/swagger/v1_json.tmpl

@ -15592,6 +15592,11 @@
"type": "boolean", "type": "boolean",
"x-go-name": "EnableIssueDependencies" "x-go-name": "EnableIssueDependencies"
}, },
"enable_issue_parents": {
"description": "Enable parents for issues and pull requests (Built-in issue tracker)",
"type": "boolean",
"x-go-name": "EnableIssueParents"
},
"enable_time_tracker": { "enable_time_tracker": {
"description": "Enable time tracking (Built-in issue tracker)", "description": "Enable time tracking (Built-in issue tracker)",
"type": "boolean", "type": "boolean",

46
web_src/js/features/repo-issue.js

@ -121,6 +121,34 @@ export function initRepoIssueList() {
fullTextSearch: true, fullTextSearch: true,
}); });
$('#new-parent-drop-list')
.dropdown({
apiSettings: {
url: issueSearchUrl,
onResponse(response) {
const filteredResponse = {success: true, results: []};
const currIssueId = $('#new-parent-drop-list').data('issue-id');
// Parse the response from the api to work with our dropdown
$.each(response, (_i, issue) => {
// Don't list current issue in the parent list.
if (issue.id === currIssueId) {
return;
}
filteredResponse.results.push({
name: `#${issue.number} ${htmlEscape(issue.title)
}<div class="text small dont-break-out">${htmlEscape(issue.repository.full_name)}</div>`,
value: issue.id,
});
});
return filteredResponse;
},
cache: false,
},
fullTextSearch: true,
});
function excludeLabel(item) { function excludeLabel(item) {
const href = $(item).attr('href'); const href = $(item).attr('href');
const id = $(item).data('label-id'); const id = $(item).data('label-id');
@ -196,6 +224,24 @@ export function initRepoIssueDependencyDelete() {
}); });
} }
export function initRepoIssueParentDelete() {
// Delete Issue parent
$(document).on('click', '.delete-parent-button', (e) => {
const id = e.currentTarget.getAttribute('data-id');
const type = e.currentTarget.getAttribute('data-type');
$('.remove-parent').modal({
closable: false,
duration: 200,
onApprove: () => {
$('#removeParentID').val(id);
$('#parentType').val(type);
$('#removeParentForm').trigger('submit');
},
}).modal('show');
});
}
export function initRepoIssueCodeCommentCancel() { export function initRepoIssueCodeCommentCancel() {
// Cancel inline code comment // Cancel inline code comment
$(document).on('click', '.cancel-code-comment', (e) => { $(document).on('click', '.cancel-code-comment', (e) => {

3
web_src/js/features/repo-legacy.js

@ -4,7 +4,7 @@ import {initCompImagePaste, initEasyMDEImagePaste} from './comp/ImagePaste.js';
import { import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
initRepoIssueCommentDelete, initRepoIssueCommentDelete,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueParentDelete,
initRepoIssueReferenceIssue, initRepoIssueStatusButton, initRepoIssueReferenceIssue, initRepoIssueStatusButton,
initRepoIssueTitleEdit, initRepoIssueTitleEdit,
initRepoIssueWipToggle, initRepoPullRequestMerge, initRepoPullRequestUpdate, initRepoIssueWipToggle, initRepoPullRequestMerge, initRepoPullRequestUpdate,
@ -516,6 +516,7 @@ export function initRepository() {
initRepoIssueCommentDelete(); initRepoIssueCommentDelete();
initRepoIssueDependencyDelete(); initRepoIssueDependencyDelete();
initRepoIssueParentDelete();
initRepoIssueCodeCommentCancel(); initRepoIssueCodeCommentCancel();
initRepoIssueStatusButton(); initRepoIssueStatusButton();
initRepoPullRequestMerge(); initRepoPullRequestMerge();

15
web_src/less/_repository.less

@ -2929,6 +2929,21 @@ tbody.commit-list {
} }
} }
#new-parent-drop-list {
&.ui.selection.dropdown {
min-width: 0;
width: 100%;
border-radius: 4px 0 0 4px;
border-right: 0;
white-space: nowrap;
}
.text {
width: 100%;
overflow: hidden;
}
}
#manage_topic { #manage_topic {
font-size: 12px; font-size: 12px;
} }

Loading…
Cancel
Save