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.
;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
;;

2
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

85
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)
}
// __________ .__
// \______ \ _______ _|__| ______ _ __
// | _// __ \ \/ / |/ __ \ \/ \/ /

52
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")

38
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

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

1
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 {

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

1
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.

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

1
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()

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

4
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,
},

4
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)

2
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

30
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=Длина очереди

2
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()

71
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

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["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)

1
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)

5
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)

1
services/forms/repo_form.go

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

2
templates/admin/config.tmpl

@ -186,6 +186,8 @@
<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
<dt>{{.i18n.Tr "admin.config.default_enable_dependencies"}}</dt>
<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>
<dt>{{.i18n.Tr "admin.config.active_code_lives"}}</dt>
<dd>{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}</dd>

2
templates/repo/header.tmpl

@ -2,7 +2,7 @@
{{with .Repository}}
<div class="ui container">
<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">
{{$avatar := (repoAvatar . 32 "mr-3")}}
{{if $avatar}}

1
templates/repo/issue/navbar.tmpl

@ -1,4 +1,5 @@
<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 .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>

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

@ -443,6 +443,131 @@
</div>
{{end}}
</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}}
<div class="ui divider"></div>

6
templates/repo/settings/options.tmpl

@ -329,6 +329,12 @@
<label>{{.i18n.Tr "repo.issues.dependency.setting"}}</label>
</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">
<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>

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",
"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",

46
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)
}<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) {
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) => {

3
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();

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 {
font-size: 12px;
}

Loading…
Cancel
Save