diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 8d7946145f..dcced2772d 100644
--- a/custom/conf/app.example.ini
+++ b/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.
;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_USER_HEATMAP = true
;;
diff --git a/integrations/api_repo_edit_test.go b/integrations/api_repo_edit_test.go
index 91ec4c699e..3f428065ef 100644
--- a/integrations/api_repo_edit_test.go
+++ b/integrations/api_repo_edit_test.go
@@ -35,6 +35,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies,
+ EnableIssueParents: config.EnableParents,
}
} else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig()
@@ -182,6 +183,7 @@ func TestAPIRepoEdit(t *testing.T) {
EnableTimeTracker: false,
AllowOnlyContributorsToTrackTime: false,
EnableIssueDependencies: false,
+ EnableIssueParents: false,
}
*repoEditOption.HasWiki = true
repoEditOption.ExternalWiki = nil
diff --git a/models/error.go b/models/error.go
index 1d0f658eb8..1afac85109 100644
--- a/models/error.go
+++ b/models/error.go
@@ -1340,6 +1340,91 @@ func (err ErrUnknownDependencyType) Error() string {
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)
+}
+
// __________ .__
// \______ \ _______ _|__| ______ _ __
// | _// __ \ \/ / |/ __ \ \/ \/ /
diff --git a/models/issue.go b/models/issue.go
index f2552c0a1e..e4dd358ad9 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1990,6 +1990,12 @@ type DependencyInfo struct {
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
func (issue *Issue) getParticipantIDsByIssue(e db.Engine) ([]int64, error) {
if issue == nil {
@@ -2048,6 +2054,42 @@ func (issue *Issue) getBlockingDependencies(e db.Engine) (issueDeps []*Dependenc
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
func (issue *Issue) BlockedByDependencies() ([]*DependencyInfo, error) {
return issue.getBlockedByDependencies(db.GetEngine(db.DefaultContext))
@@ -2058,6 +2100,16 @@ func (issue *Issue) BlockingDependencies() ([]*DependencyInfo, error) {
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) {
if issue.IsPull {
err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls")
diff --git a/models/issue_comment.go b/models/issue_comment.go
index f4a6b3ce13..8b6b204008 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -82,6 +82,10 @@ const (
CommentTypeAddDependency
// 20 Dependency removed
CommentTypeRemoveDependency
+ // 19 Parent added
+ CommentTypeAddParent
+ // 20 Parent removed
+ CommentTypeRemoveParent
// 21 Comment a line of code
CommentTypeCode
// 22 Reviews a pull request by giving general feedback
@@ -933,6 +937,39 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is
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
type CreateCommentOptions struct {
Type CommentType
@@ -942,6 +979,7 @@ type CreateCommentOptions struct {
Label *Label
DependentIssueID int64
+ ParentIssueID int64
OldMilestoneID int64
MilestoneID int64
OldProjectID int64
diff --git a/models/issue_parent.go b/models/issue_parent.go
new file mode 100644
index 0000000000..9e4780fbfb
--- /dev/null
+++ b/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
+}
diff --git a/models/issue_parent_test.go b/models/issue_parent_test.go
new file mode 100644
index 0000000000..1fd28fd47b
--- /dev/null
+++ b/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)
+}
diff --git a/models/migrations/v70.go b/models/migrations/v70.go
index 7d34c89d11..868d43295b 100644
--- a/models/migrations/v70.go
+++ b/models/migrations/v70.go
@@ -102,6 +102,9 @@ func addIssueDependencies(x *xorm.Engine) (err error) {
if _, ok := unit.Config["EnableDependencies"]; !ok {
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 {
return err
}
diff --git a/models/repo.go b/models/repo.go
index 27ae10688a..606a89008c 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -515,6 +515,7 @@ func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
EnableDependencies: setting.Service.DefaultEnableDependencies,
+ EnableParents: setting.Service.DefaultEnableParents,
},
})
} else if tp == unit.TypePullRequests {
diff --git a/models/repo/issue.go b/models/repo/issue.go
index 9f0fa3bad9..e09532ca60 100644
--- a/models/repo/issue.go
+++ b/models/repo/issue.go
@@ -70,3 +70,19 @@ func (repo *Repository) IsDependenciesEnabledCtx(ctx context.Context) bool {
}
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
+}
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index f526cbdf8b..2414c43462 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -94,6 +94,7 @@ type IssuesConfig struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableDependencies bool
+ EnableParents bool
}
// FromDB fills up a IssuesConfig from serialized format.
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 4eeab710ff..cd5259d4f8 100644
--- a/modules/context/repo.go
+++ b/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)
}
+// 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
func (r *Repository) GetCommitsCount() (int64, error) {
var contextName string
diff --git a/modules/convert/repository.go b/modules/convert/repository.go
index 459f98f396..ee320eaf6f 100644
--- a/modules/convert/repository.go
+++ b/modules/convert/repository.go
@@ -51,6 +51,7 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies,
+ EnableIssueParents: config.EnableParents,
}
} else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig()
diff --git a/modules/doctor/fix16961.go b/modules/doctor/fix16961.go
index 56d02ae92e..7843645d00 100644
--- a/modules/doctor/fix16961.go
+++ b/modules/doctor/fix16961.go
@@ -206,6 +206,10 @@ func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool,
if parseErr != nil {
return
}
+ cfg.EnableParents, parseErr = parseBool16961(parts[3])
+ if parseErr != nil {
+ return
+ }
return true, nil
}
diff --git a/modules/doctor/fix16961_test.go b/modules/doctor/fix16961_test.go
index f5e5667c09..380833cd1c 100644
--- a/modules/doctor/fix16961_test.go
+++ b/modules/doctor/fix16961_test.go
@@ -237,11 +237,12 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
}{
{
name: "normal",
- bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true}`,
+ bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true,"EnableParents":true}`,
expected: repo_model.IssuesConfig{
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
+ EnableParents: true,
},
},
{
@@ -251,6 +252,7 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
+ EnableParents: true,
},
wantFixed: true,
},
diff --git a/modules/setting/service.go b/modules/setting/service.go
index a391926382..6505f444a4 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -53,7 +53,9 @@ var Service = struct {
EnableTimetracking bool
DefaultEnableTimetracking bool
DefaultEnableDependencies bool
+ DefaultEnableParents bool
AllowCrossRepositoryDependencies bool
+ AllowCrossRepositoryParents bool
DefaultAllowOnlyContributorsToTrackTime bool
NoReplyAddress string
EnableUserHeatmap bool
@@ -141,7 +143,9 @@ func newService() {
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").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.AllowCrossRepositoryParents = sec.Key("ALLOW_CROSS_REPOSITORY_PARENTS").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.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index b8f72a411c..4ef1d5a2f6 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -25,6 +25,8 @@ type InternalTracker struct {
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
// Enable dependencies for issues and pull requests (Built-in issue tracker)
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
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 1977db5b6d..301b8702e6 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -991,6 +991,8 @@ file_view_raw=Посмотреть исходник
file_permalink=Постоянная ссылка
file_too_large=Этот файл слишком большой, поэтому он не может быть отображён.
+issues_tree=Дерево задач
+
file_copy_permalink=Копировать постоянную ссылку
video_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'video' тэг.
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_cannot_create_circular=Вы не можете создать зависимость с двумя задачами, блокирующими друг друга.
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.rejection=Невозможно запрашивать изменения своего запроса на слияние.
issues.review.approve=одобрил(а) эти изменения %s
@@ -2623,6 +2652,7 @@ config.default_allow_only_contributors_to_track_time=Учитывать толь
config.no_reply_address=No-reply адрес
config.default_visibility_organization=Видимость по умолчанию для новых организаций
config.default_enable_dependencies=Включение зависимостей для задач по умолчанию
+config.default_enable_parents=Включение родителей для задач по умолчанию
config.webhook_config=Конфигурация вебхуков
config.queue_length=Длина очереди
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 851505fe4a..7449ecabcf 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -776,6 +776,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
+ EnableParents: opts.InternalTracker.EnableIssueParents,
}
} 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
@@ -783,6 +784,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
+ EnableParents: true,
}
} else {
config = unit.IssuesConfig()
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 248743471b..7ec1af728f 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -47,6 +47,7 @@ const (
tplAttachment base.TplName = "repo/issue/view_content/attachments"
tplIssues base.TplName = "repo/issue/list"
+ tplIssuesTree base.TplName = "repo/issue/tree"
tplIssueNew base.TplName = "repo/issue/new"
tplIssueChoose base.TplName = "repo/issue/choose"
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
viewType := ctx.FormString("type")
sortType := ctx.FormString("sort")
@@ -210,7 +211,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
} else {
total = int(issueStats.ClosedCount)
}
- pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
+ pager := context.NewPagination(total, page_size, page, 5)
var mileIDs []int64
if milestoneID > 0 {
@@ -224,7 +225,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
issues, err = models.Issues(&models.IssuesOptions{
ListOptions: db.ListOptions{
Page: pager.Paginater.Current(),
- PageSize: setting.UI.IssuePagingNum,
+ PageSize: page_size,
},
RepoID: repo.ID,
AssigneeID: assigneeID,
@@ -391,7 +392,7 @@ func Issues(ctx *context.Context) {
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() {
return
}
@@ -412,6 +413,47 @@ func Issues(ctx *context.Context) {
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
func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
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
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
}
@@ -1324,6 +1369,12 @@ func ViewIssue(ctx *context.Context) {
// check if dependencies can be created across repositories
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 {
ctx.ServerError("roleDescriptor", err)
return
@@ -1650,6 +1701,18 @@ func ViewIssue(ctx *context.Context) {
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["NumParticipants"] = len(participants)
ctx.Data["Issue"] = issue
diff --git a/routers/web/repo/issue_parent.go b/routers/web/repo/issue_parent.go
new file mode 100644
index 0000000000..355d5272cc
--- /dev/null
+++ b/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)
+}
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index df5fd411b4..e8660392d8 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -289,7 +289,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Title"] = milestone.Name
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["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index f89bffb00f..f0548aaffe 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -434,6 +434,7 @@ func SettingsPost(ctx *context.Context) {
EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies,
+ EnableParents: form.EnableIssueParents,
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
diff --git a/routers/web/web.go b/routers/web/web.go
index ef1b83ed7d..df09957b4a 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -741,6 +741,10 @@ func RegisterRoutes(m *web.Route) {
m.Post("/add", repo.AddDependency)
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.Group("/times", func() {
m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
@@ -881,6 +885,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/{username}/{reponame}", func() {
m.Group("", func() {
m.Get("/{type:issues|pulls}", repo.Issues)
+ m.Get("/issues_tree", repo.IssuesTree)
m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue)
m.Group("/{type:issues|pulls}/{index}/content-history", func() {
m.Get("/overview", repo.GetContentHistoryOverview)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 3fab9af1f1..550375bd7a 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -155,6 +155,7 @@ type RepoSettingForm struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool
+ EnableIssueParents bool
IsArchived bool
// Signing Settings
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 2a27baf535..e31fe3cae6 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -186,6 +186,8 @@
{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}
{{.i18n.Tr "admin.config.default_enable_dependencies"}}
{{if .Service.DefaultEnableDependencies}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+ {{.i18n.Tr "admin.config.default_enable_parents"}}
+ {{if .Service.DefaultEnableParents}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{.i18n.Tr "admin.config.active_code_lives"}}
{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index c74d463325..13c76de5aa 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -2,7 +2,7 @@
{{with .Repository}}
+
+
+
+ {{.i18n.Tr "repo.issues.parent.setting"}}
+
+
{{.i18n.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}
diff --git a/templates/shared/issuelistfortree.tmpl b/templates/shared/issuelistfortree.tmpl
new file mode 100644
index 0000000000..faf20964bc
--- /dev/null
+++ b/templates/shared/issuelistfortree.tmpl
@@ -0,0 +1,144 @@
+
+ {{ $approvalCounts := .ApprovalCounts}}
+ {{range .Issues}}
+
+ {{end}}
+
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 791ada03fd..1d45d6e59c 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -15592,6 +15592,11 @@
"type": "boolean",
"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": {
"description": "Enable time tracking (Built-in issue tracker)",
"type": "boolean",
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 9ee5e4f04e..a663e79880 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -121,6 +121,34 @@ export function initRepoIssueList() {
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)
+ }
${htmlEscape(issue.repository.full_name)}
`,
+ value: issue.id,
+ });
+ });
+ return filteredResponse;
+ },
+ cache: false,
+ },
+
+ fullTextSearch: true,
+ });
+
+
function excludeLabel(item) {
const href = $(item).attr('href');
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() {
// Cancel inline code comment
$(document).on('click', '.cancel-code-comment', (e) => {
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index f30345bfee..b8aea6268c 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -4,7 +4,7 @@ import {initCompImagePaste, initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
initRepoIssueCommentDelete,
- initRepoIssueComments, initRepoIssueDependencyDelete,
+ initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueParentDelete,
initRepoIssueReferenceIssue, initRepoIssueStatusButton,
initRepoIssueTitleEdit,
initRepoIssueWipToggle, initRepoPullRequestMerge, initRepoPullRequestUpdate,
@@ -516,6 +516,7 @@ export function initRepository() {
initRepoIssueCommentDelete();
initRepoIssueDependencyDelete();
+ initRepoIssueParentDelete();
initRepoIssueCodeCommentCancel();
initRepoIssueStatusButton();
initRepoPullRequestMerge();
diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less
index 6cf70abdf7..897d94a401 100644
--- a/web_src/less/_repository.less
+++ b/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 {
font-size: 12px;
}