mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-20 00:50:10 +02:00
Merge branch 'main' into feat-32257-add-comments-unchanged-lines-and-show
This commit is contained in:
commit
914bed86ab
@ -1007,6 +1007,14 @@ LEVEL = Info
|
|||||||
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
|
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
|
||||||
;DEFAULT_FORK_REPO_UNITS = repo.code,repo.pulls
|
;DEFAULT_FORK_REPO_UNITS = repo.code,repo.pulls
|
||||||
;;
|
;;
|
||||||
|
;; Comma separated list of default mirror repo units.
|
||||||
|
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
|
||||||
|
;DEFAULT_MIRROR_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.wiki,repo.projects,repo.packages
|
||||||
|
;;
|
||||||
|
;; Comma separated list of default template repo units.
|
||||||
|
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
|
||||||
|
;DEFAULT_TEMPLATE_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki,repo.projects,repo.packages
|
||||||
|
;;
|
||||||
;; Prefix archive files by placing them in a directory named after the repository
|
;; Prefix archive files by placing them in a directory named after the repository
|
||||||
;PREFIX_ARCHIVE_FILES = true
|
;PREFIX_ARCHIVE_FILES = true
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -141,8 +141,9 @@ func (org *Organization) LoadTeams(ctx context.Context) ([]*Team, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMembers returns all members of organization.
|
// GetMembers returns all members of organization.
|
||||||
func (org *Organization) GetMembers(ctx context.Context) (user_model.UserList, map[int64]bool, error) {
|
func (org *Organization) GetMembers(ctx context.Context, doer *user_model.User) (user_model.UserList, map[int64]bool, error) {
|
||||||
return FindOrgMembers(ctx, &FindOrgMembersOpts{
|
return FindOrgMembers(ctx, &FindOrgMembersOpts{
|
||||||
|
Doer: doer,
|
||||||
OrgID: org.ID,
|
OrgID: org.ID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -195,16 +196,22 @@ func (org *Organization) CanCreateRepo() bool {
|
|||||||
// FindOrgMembersOpts represensts find org members conditions
|
// FindOrgMembersOpts represensts find org members conditions
|
||||||
type FindOrgMembersOpts struct {
|
type FindOrgMembersOpts struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
OrgID int64
|
Doer *user_model.User
|
||||||
PublicOnly bool
|
IsDoerMember bool
|
||||||
|
OrgID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts FindOrgMembersOpts) PublicOnly() bool {
|
||||||
|
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountOrgMembers counts the organization's members
|
// CountOrgMembers counts the organization's members
|
||||||
func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, error) {
|
func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, error) {
|
||||||
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
||||||
if opts.PublicOnly {
|
if opts.PublicOnly() {
|
||||||
sess.And("is_public = ?", true)
|
sess.And("is_public = ?", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess.Count(new(OrgUser))
|
return sess.Count(new(OrgUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,9 +532,10 @@ func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organiz
|
|||||||
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
|
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
|
||||||
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
|
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
|
||||||
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
||||||
if opts.PublicOnly {
|
if opts.PublicOnly() {
|
||||||
sess.And("is_public = ?", true)
|
sess.And("is_public = ?", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ListOptions.PageSize > 0 {
|
if opts.ListOptions.PageSize > 0 {
|
||||||
sess = db.SetSessionPagination(sess, opts)
|
sess = db.SetSessionPagination(sess, opts)
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
package organization_test
|
package organization_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@ -103,7 +104,7 @@ func TestUser_GetTeams(t *testing.T) {
|
|||||||
func TestUser_GetMembers(t *testing.T) {
|
func TestUser_GetMembers(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||||
members, _, err := org.GetMembers(db.DefaultContext)
|
members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.Len(t, members, 3) {
|
if assert.Len(t, members, 3) {
|
||||||
assert.Equal(t, int64(2), members[0].ID)
|
assert.Equal(t, int64(2), members[0].ID)
|
||||||
@ -210,37 +211,42 @@ func TestFindOrgs(t *testing.T) {
|
|||||||
func TestGetOrgUsersByOrgID(t *testing.T) {
|
func TestGetOrgUsersByOrgID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
|
opts := &organization.FindOrgMembersOpts{
|
||||||
ListOptions: db.ListOptions{},
|
Doer: &user_model.User{IsAdmin: true},
|
||||||
OrgID: 3,
|
OrgID: 3,
|
||||||
PublicOnly: false,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
if assert.Len(t, orgUsers, 3) {
|
|
||||||
assert.Equal(t, organization.OrgUser{
|
|
||||||
ID: orgUsers[0].ID,
|
|
||||||
OrgID: 3,
|
|
||||||
UID: 2,
|
|
||||||
IsPublic: true,
|
|
||||||
}, *orgUsers[0])
|
|
||||||
assert.Equal(t, organization.OrgUser{
|
|
||||||
ID: orgUsers[1].ID,
|
|
||||||
OrgID: 3,
|
|
||||||
UID: 4,
|
|
||||||
IsPublic: false,
|
|
||||||
}, *orgUsers[1])
|
|
||||||
assert.Equal(t, organization.OrgUser{
|
|
||||||
ID: orgUsers[2].ID,
|
|
||||||
OrgID: 3,
|
|
||||||
UID: 28,
|
|
||||||
IsPublic: true,
|
|
||||||
}, *orgUsers[2])
|
|
||||||
}
|
}
|
||||||
|
assert.False(t, opts.PublicOnly())
|
||||||
|
orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
sort.Slice(orgUsers, func(i, j int) bool {
|
||||||
|
return orgUsers[i].ID < orgUsers[j].ID
|
||||||
|
})
|
||||||
|
assert.EqualValues(t, []*organization.OrgUser{{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 3,
|
||||||
|
UID: 2,
|
||||||
|
IsPublic: true,
|
||||||
|
}, {
|
||||||
|
ID: 2,
|
||||||
|
OrgID: 3,
|
||||||
|
UID: 4,
|
||||||
|
IsPublic: false,
|
||||||
|
}, {
|
||||||
|
ID: 9,
|
||||||
|
OrgID: 3,
|
||||||
|
UID: 28,
|
||||||
|
IsPublic: true,
|
||||||
|
}}, orgUsers)
|
||||||
|
|
||||||
|
opts = &organization.FindOrgMembersOpts{OrgID: 3}
|
||||||
|
assert.True(t, opts.PublicOnly())
|
||||||
|
orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, orgUsers, 2)
|
||||||
|
|
||||||
orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
|
orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
|
||||||
ListOptions: db.ListOptions{},
|
ListOptions: db.ListOptions{},
|
||||||
OrgID: unittest.NonexistentID,
|
OrgID: unittest.NonexistentID,
|
||||||
PublicOnly: false,
|
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, orgUsers, 0)
|
assert.Len(t, orgUsers, 0)
|
||||||
|
|||||||
@ -94,7 +94,7 @@ func TestUserListIsPublicMember(t *testing.T) {
|
|||||||
func testUserListIsPublicMember(t *testing.T, orgID int64, expected map[int64]bool) {
|
func testUserListIsPublicMember(t *testing.T, orgID int64, expected map[int64]bool) {
|
||||||
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
|
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, membersIsPublic, err := org.GetMembers(db.DefaultContext)
|
_, membersIsPublic, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, membersIsPublic)
|
assert.Equal(t, expected, membersIsPublic)
|
||||||
}
|
}
|
||||||
@ -121,7 +121,7 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
|
|||||||
func testUserListIsUserOrgOwner(t *testing.T, orgID int64, expected map[int64]bool) {
|
func testUserListIsUserOrgOwner(t *testing.T, orgID int64, expected map[int64]bool) {
|
||||||
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
|
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
members, _, err := org.GetMembers(db.DefaultContext)
|
members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, organization.IsUserOrgOwner(db.DefaultContext, members, orgID))
|
assert.Equal(t, expected, organization.IsUserOrgOwner(db.DefaultContext, members, orgID))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,6 +80,27 @@ var (
|
|||||||
TypePullRequests,
|
TypePullRequests,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultMirrorRepoUnits contains the default unit types for mirrors
|
||||||
|
DefaultMirrorRepoUnits = []Type{
|
||||||
|
TypeCode,
|
||||||
|
TypeIssues,
|
||||||
|
TypeReleases,
|
||||||
|
TypeWiki,
|
||||||
|
TypeProjects,
|
||||||
|
TypePackages,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTemplateRepoUnits contains the default unit types for templates
|
||||||
|
DefaultTemplateRepoUnits = []Type{
|
||||||
|
TypeCode,
|
||||||
|
TypeIssues,
|
||||||
|
TypePullRequests,
|
||||||
|
TypeReleases,
|
||||||
|
TypeWiki,
|
||||||
|
TypeProjects,
|
||||||
|
TypePackages,
|
||||||
|
}
|
||||||
|
|
||||||
// NotAllowedDefaultRepoUnits contains units that can't be default
|
// NotAllowedDefaultRepoUnits contains units that can't be default
|
||||||
NotAllowedDefaultRepoUnits = []Type{
|
NotAllowedDefaultRepoUnits = []Type{
|
||||||
TypeExternalWiki,
|
TypeExternalWiki,
|
||||||
@ -147,6 +168,7 @@ func LoadUnitConfig() error {
|
|||||||
if len(DefaultRepoUnits) == 0 {
|
if len(DefaultRepoUnits) == 0 {
|
||||||
return errors.New("no default repository units found")
|
return errors.New("no default repository units found")
|
||||||
}
|
}
|
||||||
|
// default fork repo units
|
||||||
setDefaultForkRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultForkRepoUnits...)
|
setDefaultForkRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultForkRepoUnits...)
|
||||||
if len(invalidKeys) > 0 {
|
if len(invalidKeys) > 0 {
|
||||||
log.Warn("Invalid keys in default fork repo units: %s", strings.Join(invalidKeys, ", "))
|
log.Warn("Invalid keys in default fork repo units: %s", strings.Join(invalidKeys, ", "))
|
||||||
@ -155,6 +177,24 @@ func LoadUnitConfig() error {
|
|||||||
if len(DefaultForkRepoUnits) == 0 {
|
if len(DefaultForkRepoUnits) == 0 {
|
||||||
return errors.New("no default fork repository units found")
|
return errors.New("no default fork repository units found")
|
||||||
}
|
}
|
||||||
|
// default mirror repo units
|
||||||
|
setDefaultMirrorRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultMirrorRepoUnits...)
|
||||||
|
if len(invalidKeys) > 0 {
|
||||||
|
log.Warn("Invalid keys in default mirror repo units: %s", strings.Join(invalidKeys, ", "))
|
||||||
|
}
|
||||||
|
DefaultMirrorRepoUnits = validateDefaultRepoUnits(DefaultMirrorRepoUnits, setDefaultMirrorRepoUnits)
|
||||||
|
if len(DefaultMirrorRepoUnits) == 0 {
|
||||||
|
return errors.New("no default mirror repository units found")
|
||||||
|
}
|
||||||
|
// default template repo units
|
||||||
|
setDefaultTemplateRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultTemplateRepoUnits...)
|
||||||
|
if len(invalidKeys) > 0 {
|
||||||
|
log.Warn("Invalid keys in default template repo units: %s", strings.Join(invalidKeys, ", "))
|
||||||
|
}
|
||||||
|
DefaultTemplateRepoUnits = validateDefaultRepoUnits(DefaultTemplateRepoUnits, setDefaultTemplateRepoUnits)
|
||||||
|
if len(DefaultTemplateRepoUnits) == 0 {
|
||||||
|
return errors.New("no default template repository units found")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -147,6 +147,9 @@ func StringsToInt64s(strs []string) ([]int64, error) {
|
|||||||
}
|
}
|
||||||
ints := make([]int64, 0, len(strs))
|
ints := make([]int64, 0, len(strs))
|
||||||
for _, s := range strs {
|
for _, s := range strs {
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
n, err := strconv.ParseInt(s, 10, 64)
|
n, err := strconv.ParseInt(s, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -152,6 +152,7 @@ func TestStringsToInt64s(t *testing.T) {
|
|||||||
}
|
}
|
||||||
testSuccess(nil, nil)
|
testSuccess(nil, nil)
|
||||||
testSuccess([]string{}, []int64{})
|
testSuccess([]string{}, []int64{})
|
||||||
|
testSuccess([]string{""}, []int64{})
|
||||||
testSuccess([]string{"-1234"}, []int64{-1234})
|
testSuccess([]string{"-1234"}, []int64{-1234})
|
||||||
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
|
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
|
||||||
|
|
||||||
|
|||||||
@ -31,8 +31,8 @@ func (s Set[T]) AddMultiple(values ...T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains determines whether a set contains the specified elements.
|
// Contains determines whether a set contains all these elements.
|
||||||
// Returns true if the set contains the specified element; otherwise, false.
|
// Returns true if the set contains all these elements; otherwise, false.
|
||||||
func (s Set[T]) Contains(values ...T) bool {
|
func (s Set[T]) Contains(values ...T) bool {
|
||||||
ret := true
|
ret := true
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
|
|||||||
@ -18,7 +18,9 @@ func TestSet(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, s.Contains("key1"))
|
assert.True(t, s.Contains("key1"))
|
||||||
assert.True(t, s.Contains("key2"))
|
assert.True(t, s.Contains("key2"))
|
||||||
|
assert.True(t, s.Contains("key1", "key2"))
|
||||||
assert.False(t, s.Contains("key3"))
|
assert.False(t, s.Contains("key3"))
|
||||||
|
assert.False(t, s.Contains("key1", "key3"))
|
||||||
|
|
||||||
assert.True(t, s.Remove("key2"))
|
assert.True(t, s.Remove("key2"))
|
||||||
assert.False(t, s.Contains("key2"))
|
assert.False(t, s.Contains("key2"))
|
||||||
|
|||||||
@ -43,6 +43,8 @@ var (
|
|||||||
DisabledRepoUnits []string
|
DisabledRepoUnits []string
|
||||||
DefaultRepoUnits []string
|
DefaultRepoUnits []string
|
||||||
DefaultForkRepoUnits []string
|
DefaultForkRepoUnits []string
|
||||||
|
DefaultMirrorRepoUnits []string
|
||||||
|
DefaultTemplateRepoUnits []string
|
||||||
PrefixArchiveFiles bool
|
PrefixArchiveFiles bool
|
||||||
DisableMigrations bool
|
DisableMigrations bool
|
||||||
DisableStars bool `ini:"DISABLE_STARS"`
|
DisableStars bool `ini:"DISABLE_STARS"`
|
||||||
@ -161,6 +163,8 @@ var (
|
|||||||
DisabledRepoUnits: []string{},
|
DisabledRepoUnits: []string{},
|
||||||
DefaultRepoUnits: []string{},
|
DefaultRepoUnits: []string{},
|
||||||
DefaultForkRepoUnits: []string{},
|
DefaultForkRepoUnits: []string{},
|
||||||
|
DefaultMirrorRepoUnits: []string{},
|
||||||
|
DefaultTemplateRepoUnits: []string{},
|
||||||
PrefixArchiveFiles: true,
|
PrefixArchiveFiles: true,
|
||||||
DisableMigrations: false,
|
DisableMigrations: false,
|
||||||
DisableStars: false,
|
DisableStars: false,
|
||||||
|
|||||||
@ -86,7 +86,9 @@ type CreatePullRequestOption struct {
|
|||||||
Milestone int64 `json:"milestone"`
|
Milestone int64 `json:"milestone"`
|
||||||
Labels []int64 `json:"labels"`
|
Labels []int64 `json:"labels"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
Deadline *time.Time `json:"due_date"`
|
Deadline *time.Time `json:"due_date"`
|
||||||
|
Reviewers []string `json:"reviewers"`
|
||||||
|
TeamReviewers []string `json:"team_reviewers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditPullRequestOption options when modify pull request
|
// EditPullRequestOption options when modify pull request
|
||||||
|
|||||||
@ -31,6 +31,7 @@ func NewFuncMap() template.FuncMap {
|
|||||||
"ctx": func() any { return nil }, // template context function
|
"ctx": func() any { return nil }, // template context function
|
||||||
|
|
||||||
"DumpVar": dumpVar,
|
"DumpVar": dumpVar,
|
||||||
|
"NIL": func() any { return nil },
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// html/template related functions
|
// html/template related functions
|
||||||
|
|||||||
@ -1462,7 +1462,7 @@ issues.new.closed_milestone = Closed Milestones
|
|||||||
issues.new.assignees = Assignees
|
issues.new.assignees = Assignees
|
||||||
issues.new.clear_assignees = Clear assignees
|
issues.new.clear_assignees = Clear assignees
|
||||||
issues.new.no_assignees = No Assignees
|
issues.new.no_assignees = No Assignees
|
||||||
issues.new.no_reviewers = No reviewers
|
issues.new.no_reviewers = No Reviewers
|
||||||
issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
|
issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
|
||||||
issues.edit.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
|
issues.edit.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
|
||||||
issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
|
issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
|
||||||
|
|||||||
@ -18,11 +18,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// listMembers list an organization's members
|
// listMembers list an organization's members
|
||||||
func listMembers(ctx *context.APIContext, publicOnly bool) {
|
func listMembers(ctx *context.APIContext, isMember bool) {
|
||||||
opts := &organization.FindOrgMembersOpts{
|
opts := &organization.FindOrgMembersOpts{
|
||||||
OrgID: ctx.Org.Organization.ID,
|
Doer: ctx.Doer,
|
||||||
PublicOnly: publicOnly,
|
IsDoerMember: isMember,
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
OrgID: ctx.Org.Organization.ID,
|
||||||
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := organization.CountOrgMembers(ctx, opts)
|
count, err := organization.CountOrgMembers(ctx, opts)
|
||||||
@ -73,16 +74,19 @@ func ListMembers(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
publicOnly := true
|
var (
|
||||||
|
isMember bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
if ctx.Doer != nil {
|
if ctx.Doer != nil {
|
||||||
isMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
|
isMember, err = ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
publicOnly = !isMember && !ctx.Doer.IsAdmin
|
|
||||||
}
|
}
|
||||||
listMembers(ctx, publicOnly)
|
listMembers(ctx, isMember)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPublicMembers list an organization's public members
|
// ListPublicMembers list an organization's public members
|
||||||
@ -112,7 +116,7 @@ func ListPublicMembers(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
listMembers(ctx, true)
|
listMembers(ctx, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMember check if a user is a member of an organization
|
// IsMember check if a user is a member of an organization
|
||||||
|
|||||||
@ -554,7 +554,19 @@ func CreatePullRequest(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
|
prOpts := &pull_service.NewPullRequestOptions{
|
||||||
|
Repo: repo,
|
||||||
|
Issue: prIssue,
|
||||||
|
LabelIDs: labelIDs,
|
||||||
|
PullRequest: pr,
|
||||||
|
AssigneeIDs: assigneeIDs,
|
||||||
|
}
|
||||||
|
prOpts.Reviewers, prOpts.TeamReviewers = parseReviewersByNames(ctx, form.Reviewers, form.TeamReviewers)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||||
|
|||||||
@ -656,6 +656,47 @@ func DeleteReviewRequests(ctx *context.APIContext) {
|
|||||||
apiReviewRequest(ctx, *opts, false)
|
apiReviewRequest(ctx, *opts, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerNames []string) (reviewers []*user_model.User, teamReviewers []*organization.Team) {
|
||||||
|
var err error
|
||||||
|
for _, r := range reviewerNames {
|
||||||
|
var reviewer *user_model.User
|
||||||
|
if strings.Contains(r, "@") {
|
||||||
|
reviewer, err = user_model.GetUserByEmail(ctx, r)
|
||||||
|
} else {
|
||||||
|
reviewer, err = user_model.GetUserByName(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetUser", err)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewers = append(reviewers, reviewer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Repo.Repository.Owner.IsOrganization() && len(teamReviewerNames) > 0 {
|
||||||
|
for _, t := range teamReviewerNames {
|
||||||
|
var teamReviewer *organization.Team
|
||||||
|
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
|
||||||
|
if err != nil {
|
||||||
|
if organization.IsErrTeamNotExist(err) {
|
||||||
|
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
teamReviewers = append(teamReviewers, teamReviewer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reviewers, teamReviewers
|
||||||
|
}
|
||||||
|
|
||||||
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
|
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
|
||||||
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -672,42 +713,15 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reviewers := make([]*user_model.User, 0, len(opts.Reviewers))
|
|
||||||
|
|
||||||
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
|
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range opts.Reviewers {
|
reviewers, teamReviewers := parseReviewersByNames(ctx, opts.Reviewers, opts.TeamReviewers)
|
||||||
var reviewer *user_model.User
|
if ctx.Written() {
|
||||||
if strings.Contains(r, "@") {
|
return
|
||||||
reviewer, err = user_model.GetUserByEmail(ctx, r)
|
|
||||||
} else {
|
|
||||||
reviewer, err = user_model.GetUserByName(ctx, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if user_model.IsErrUserNotExist(err) {
|
|
||||||
ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetUser", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
|
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrNotValidReviewRequest(err) {
|
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "IsValidReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reviewers = append(reviewers, reviewer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var reviews []*issues_model.Review
|
var reviews []*issues_model.Review
|
||||||
@ -716,12 +730,16 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, reviewer := range reviewers {
|
for _, reviewer := range reviewers {
|
||||||
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
|
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if issues_model.IsErrReviewRequestOnClosedPR(err) {
|
if issues_model.IsErrReviewRequestOnClosedPR(err) {
|
||||||
ctx.Error(http.StatusForbidden, "", err)
|
ctx.Error(http.StatusForbidden, "", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if issues_model.IsErrNotValidReviewRequest(err) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -736,35 +754,17 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 {
|
if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 {
|
||||||
teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers))
|
|
||||||
for _, t := range opts.TeamReviewers {
|
|
||||||
var teamReviewer *organization.Team
|
|
||||||
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
|
|
||||||
if err != nil {
|
|
||||||
if organization.IsErrTeamNotExist(err) {
|
|
||||||
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue)
|
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrNotValidReviewRequest(err) {
|
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "IsValidTeamReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
teamReviewers = append(teamReviewers, teamReviewer)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, teamReviewer := range teamReviewers {
|
for _, teamReviewer := range teamReviewers {
|
||||||
comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
|
comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if issues_model.IsErrReviewRequestOnClosedPR(err) {
|
||||||
|
ctx.Error(http.StatusForbidden, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if issues_model.IsErrNotValidReviewRequest(err) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("TeamReviewRequest", err)
|
ctx.ServerError("TeamReviewRequest", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,10 +95,12 @@ func home(ctx *context.Context, viewRepositories bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
opts := &organization.FindOrgMembersOpts{
|
opts := &organization.FindOrgMembersOpts{
|
||||||
OrgID: org.ID,
|
Doer: ctx.Doer,
|
||||||
PublicOnly: ctx.Org.PublicMemberOnly,
|
OrgID: org.ID,
|
||||||
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
|
IsDoerMember: ctx.Org.IsMember,
|
||||||
|
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
|
||||||
}
|
}
|
||||||
|
|
||||||
members, _, err := organization.FindOrgMembers(ctx, opts)
|
members, _, err := organization.FindOrgMembers(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("FindOrgMembers", err)
|
ctx.ServerError("FindOrgMembers", err)
|
||||||
|
|||||||
@ -34,8 +34,8 @@ func Members(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
opts := &organization.FindOrgMembersOpts{
|
opts := &organization.FindOrgMembersOpts{
|
||||||
OrgID: org.ID,
|
Doer: ctx.Doer,
|
||||||
PublicOnly: true,
|
OrgID: org.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Doer != nil {
|
if ctx.Doer != nil {
|
||||||
@ -44,9 +44,9 @@ func Members(ctx *context.Context) {
|
|||||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember")
|
ctx.Error(http.StatusInternalServerError, "IsOrgMember")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts.PublicOnly = !isMember && !ctx.Doer.IsAdmin
|
opts.IsDoerMember = isMember
|
||||||
}
|
}
|
||||||
ctx.Data["PublicOnly"] = opts.PublicOnly
|
ctx.Data["PublicOnly"] = opts.PublicOnly()
|
||||||
|
|
||||||
total, err := organization.CountOrgMembers(ctx, opts)
|
total, err := organization.CountOrgMembers(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -790,10 +790,14 @@ func CompareDiff(ctx *context.Context) {
|
|||||||
|
|
||||||
if !nothingToCompare {
|
if !nothingToCompare {
|
||||||
// Setup information for new form.
|
// Setup information for new form.
|
||||||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
|
pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, true)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
|
||||||
|
if len(templateErrs) > 0 {
|
||||||
|
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
||||||
@ -806,11 +810,6 @@ func CompareDiff(ctx *context.Context) {
|
|||||||
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
|
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
|
||||||
|
|
||||||
ctx.Data["IsDiffCompare"] = true
|
ctx.Data["IsDiffCompare"] = true
|
||||||
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
|
|
||||||
|
|
||||||
if len(templateErrs) > 0 {
|
|
||||||
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if content, ok := ctx.Data["content"].(string); ok && content != "" {
|
if content, ok := ctx.Data["content"].(string); ok && content != "" {
|
||||||
// If a template content is set, prepend the "content". In this case that's only
|
// If a template content is set, prepend the "content". In this case that's only
|
||||||
|
|||||||
@ -431,7 +431,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveProjects(ctx, repo)
|
retrieveProjectsForIssueList(ctx, repo)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -556,37 +556,147 @@ func renderMilestones(ctx *context.Context) {
|
|||||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
|
type issueSidebarMilestoneData struct {
|
||||||
func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
|
SelectedMilestoneID int64
|
||||||
|
OpenMilestones []*issues_model.Milestone
|
||||||
|
ClosedMilestones []*issues_model.Milestone
|
||||||
|
}
|
||||||
|
|
||||||
|
type issueSidebarAssigneesData struct {
|
||||||
|
SelectedAssigneeIDs string
|
||||||
|
CandidateAssignees []*user_model.User
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssuePageMetaData struct {
|
||||||
|
RepoLink string
|
||||||
|
Repository *repo_model.Repository
|
||||||
|
Issue *issues_model.Issue
|
||||||
|
IsPullRequest bool
|
||||||
|
CanModifyIssueOrPull bool
|
||||||
|
|
||||||
|
ReviewersData *issueSidebarReviewersData
|
||||||
|
LabelsData *issueSidebarLabelsData
|
||||||
|
MilestonesData *issueSidebarMilestoneData
|
||||||
|
ProjectsData *issueSidebarProjectsData
|
||||||
|
AssigneesData *issueSidebarAssigneesData
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, isPull bool) *IssuePageMetaData {
|
||||||
|
data := &IssuePageMetaData{
|
||||||
|
RepoLink: ctx.Repo.RepoLink,
|
||||||
|
Repository: repo,
|
||||||
|
Issue: issue,
|
||||||
|
IsPullRequest: isPull,
|
||||||
|
|
||||||
|
ReviewersData: &issueSidebarReviewersData{},
|
||||||
|
LabelsData: &issueSidebarLabelsData{},
|
||||||
|
MilestonesData: &issueSidebarMilestoneData{},
|
||||||
|
ProjectsData: &issueSidebarProjectsData{},
|
||||||
|
AssigneesData: &issueSidebarAssigneesData{},
|
||||||
|
}
|
||||||
|
ctx.Data["IssuePageMetaData"] = data
|
||||||
|
|
||||||
|
if isPull {
|
||||||
|
data.retrieveReviewersData(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.retrieveLabelsData(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
|
||||||
|
if !data.CanModifyIssueOrPull {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
data.retrieveAssigneesDataForIssueWriter(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
data.retrieveMilestonesDataForIssueWriter(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
data.retrieveProjectsDataForIssueWriter(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareBranchList(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Context) {
|
||||||
var err error
|
var err error
|
||||||
ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
if d.Issue != nil {
|
||||||
RepoID: repo.ID,
|
d.MilestonesData.SelectedMilestoneID = d.Issue.MilestoneID
|
||||||
|
}
|
||||||
|
d.MilestonesData.OpenMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||||
|
RepoID: d.Repository.ID,
|
||||||
IsClosed: optional.Some(false),
|
IsClosed: optional.Some(false),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetMilestones", err)
|
ctx.ServerError("GetMilestones", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
d.MilestonesData.ClosedMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: d.Repository.ID,
|
||||||
IsClosed: optional.Some(true),
|
IsClosed: optional.Some(true),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetMilestones", err)
|
ctx.ServerError("GetMilestones", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
|
func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) {
|
||||||
|
var err error
|
||||||
|
d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetRepoAssignees", err)
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
d.AssigneesData.CandidateAssignees = shared_user.MakeSelfOnTop(ctx.Doer, d.AssigneesData.CandidateAssignees)
|
||||||
|
if d.Issue != nil {
|
||||||
|
_ = d.Issue.LoadAssignees(ctx)
|
||||||
|
ids := make([]string, 0, len(d.Issue.Assignees))
|
||||||
|
for _, a := range d.Issue.Assignees {
|
||||||
|
ids = append(ids, strconv.FormatInt(a.ID, 10))
|
||||||
|
}
|
||||||
|
d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
|
||||||
|
}
|
||||||
|
// FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
|
||||||
handleTeamMentions(ctx)
|
handleTeamMentions(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) {
|
||||||
|
ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type issueSidebarProjectsData struct {
|
||||||
|
SelectedProjectID int64
|
||||||
|
OpenProjects []*project_model.Project
|
||||||
|
ClosedProjects []*project_model.Project
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
||||||
|
if d.Issue != nil && d.Issue.Project != nil {
|
||||||
|
d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
|
||||||
|
}
|
||||||
|
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) {
|
||||||
// Distinguish whether the owner of the repository
|
// Distinguish whether the owner of the repository
|
||||||
// is an individual or an organization
|
// is an individual or an organization
|
||||||
repoOwnerType := project_model.TypeIndividual
|
repoOwnerType := project_model.TypeIndividual
|
||||||
@ -609,7 +719,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjects", err)
|
ctx.ServerError("GetProjects", err)
|
||||||
return
|
return nil, nil
|
||||||
}
|
}
|
||||||
closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||||
ListOptions: db.ListOptionsAll,
|
ListOptions: db.ListOptionsAll,
|
||||||
@ -619,7 +729,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjects", err)
|
ctx.ServerError("GetProjects", err)
|
||||||
return
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,7 +742,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjects", err)
|
ctx.ServerError("GetProjects", err)
|
||||||
return
|
return nil, nil
|
||||||
}
|
}
|
||||||
openProjects = append(openProjects, openProjects2...)
|
openProjects = append(openProjects, openProjects2...)
|
||||||
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||||
@ -643,45 +753,74 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjects", err)
|
ctx.ServerError("GetProjects", err)
|
||||||
return
|
return nil, nil
|
||||||
}
|
}
|
||||||
closedProjects = append(closedProjects, closedProjects2...)
|
closedProjects = append(closedProjects, closedProjects2...)
|
||||||
}
|
}
|
||||||
|
return openProjects, closedProjects
|
||||||
ctx.Data["OpenProjects"] = openProjects
|
|
||||||
ctx.Data["ClosedProjects"] = closedProjects
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// repoReviewerSelection items to bee shown
|
// repoReviewerSelection items to bee shown
|
||||||
type repoReviewerSelection struct {
|
type repoReviewerSelection struct {
|
||||||
IsTeam bool
|
IsTeam bool
|
||||||
Team *organization.Team
|
Team *organization.Team
|
||||||
User *user_model.User
|
User *user_model.User
|
||||||
Review *issues_model.Review
|
Review *issues_model.Review
|
||||||
CanChange bool
|
CanBeDismissed bool
|
||||||
Checked bool
|
CanChange bool
|
||||||
ItemID int64
|
Requested bool
|
||||||
|
ItemID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveRepoReviewers find all reviewers of a repository
|
type issueSidebarReviewersData struct {
|
||||||
func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
|
CanChooseReviewer bool
|
||||||
ctx.Data["CanChooseReviewer"] = canChooseReviewer
|
OriginalReviews issues_model.ReviewList
|
||||||
|
TeamReviewers []*repoReviewerSelection
|
||||||
|
Reviewers []*repoReviewerSelection
|
||||||
|
CurrentPullReviewers []*repoReviewerSelection
|
||||||
|
}
|
||||||
|
|
||||||
originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID)
|
// RetrieveRepoReviewers find all reviewers of a repository. If issue is nil, it means the doer is creating a new PR.
|
||||||
if err != nil {
|
func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
|
||||||
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
|
data := d.ReviewersData
|
||||||
return
|
repo := d.Repository
|
||||||
}
|
if ctx.Doer != nil && ctx.IsSigned {
|
||||||
ctx.Data["OriginalReviews"] = originalAuthorReviews
|
if d.Issue == nil {
|
||||||
|
data.CanChooseReviewer = true
|
||||||
reviews, err := issues_model.GetReviewsByIssueID(ctx, issue.ID)
|
} else {
|
||||||
if err != nil {
|
data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue)
|
||||||
ctx.ServerError("GetReviewersByIssueID", err)
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reviews) == 0 && !canChooseReviewer {
|
var posterID int64
|
||||||
return
|
var isClosed bool
|
||||||
|
var reviews issues_model.ReviewList
|
||||||
|
|
||||||
|
if d.Issue == nil {
|
||||||
|
posterID = ctx.Doer.ID
|
||||||
|
} else {
|
||||||
|
posterID = d.Issue.PosterID
|
||||||
|
if d.Issue.OriginalAuthorID > 0 {
|
||||||
|
posterID = 0 // for migrated PRs, no poster ID
|
||||||
|
}
|
||||||
|
|
||||||
|
isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged
|
||||||
|
|
||||||
|
originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.OriginalReviews = originalAuthorReviews
|
||||||
|
|
||||||
|
reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetReviewersByIssueID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(reviews) == 0 && !data.CanChooseReviewer {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -692,12 +831,8 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
reviewers []*user_model.User
|
reviewers []*user_model.User
|
||||||
)
|
)
|
||||||
|
|
||||||
if canChooseReviewer {
|
if data.CanChooseReviewer {
|
||||||
posterID := issue.PosterID
|
var err error
|
||||||
if issue.OriginalAuthorID > 0 {
|
|
||||||
posterID = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
|
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetReviewers", err)
|
ctx.ServerError("GetReviewers", err)
|
||||||
@ -723,16 +858,16 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
|
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
tmp := &repoReviewerSelection{
|
tmp := &repoReviewerSelection{
|
||||||
Checked: review.Type == issues_model.ReviewTypeRequest,
|
Requested: review.Type == issues_model.ReviewTypeRequest,
|
||||||
Review: review,
|
Review: review,
|
||||||
ItemID: review.ReviewerID,
|
ItemID: review.ReviewerID,
|
||||||
}
|
}
|
||||||
if review.ReviewerTeamID > 0 {
|
if review.ReviewerTeamID > 0 {
|
||||||
tmp.IsTeam = true
|
tmp.IsTeam = true
|
||||||
tmp.ItemID = -review.ReviewerTeamID
|
tmp.ItemID = -review.ReviewerTeamID
|
||||||
}
|
}
|
||||||
|
|
||||||
if canChooseReviewer {
|
if data.CanChooseReviewer {
|
||||||
// Users who can choose reviewers can also remove review requests
|
// Users who can choose reviewers can also remove review requests
|
||||||
tmp.CanChange = true
|
tmp.CanChange = true
|
||||||
} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
|
} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
|
||||||
@ -742,7 +877,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
|
|
||||||
pullReviews = append(pullReviews, tmp)
|
pullReviews = append(pullReviews, tmp)
|
||||||
|
|
||||||
if canChooseReviewer {
|
if data.CanChooseReviewer {
|
||||||
if tmp.IsTeam {
|
if tmp.IsTeam {
|
||||||
teamReviewersResult = append(teamReviewersResult, tmp)
|
teamReviewersResult = append(teamReviewersResult, tmp)
|
||||||
} else {
|
} else {
|
||||||
@ -756,7 +891,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
|
currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
|
||||||
for _, item := range pullReviews {
|
for _, item := range pullReviews {
|
||||||
if item.Review.ReviewerID > 0 {
|
if item.Review.ReviewerID > 0 {
|
||||||
if err = item.Review.LoadReviewer(ctx); err != nil {
|
if err := item.Review.LoadReviewer(ctx); err != nil {
|
||||||
if user_model.IsErrUserNotExist(err) {
|
if user_model.IsErrUserNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -765,7 +900,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
}
|
}
|
||||||
item.User = item.Review.Reviewer
|
item.User = item.Review.Reviewer
|
||||||
} else if item.Review.ReviewerTeamID > 0 {
|
} else if item.Review.ReviewerTeamID > 0 {
|
||||||
if err = item.Review.LoadReviewerTeam(ctx); err != nil {
|
if err := item.Review.LoadReviewerTeam(ctx); err != nil {
|
||||||
if organization.IsErrTeamNotExist(err) {
|
if organization.IsErrTeamNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -776,13 +911,14 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
item.CanBeDismissed = ctx.Repo.Permission.IsAdmin() && !isClosed &&
|
||||||
|
(item.Review.Type == issues_model.ReviewTypeApprove || item.Review.Type == issues_model.ReviewTypeReject)
|
||||||
currentPullReviewers = append(currentPullReviewers, item)
|
currentPullReviewers = append(currentPullReviewers, item)
|
||||||
}
|
}
|
||||||
ctx.Data["PullReviewers"] = currentPullReviewers
|
data.CurrentPullReviewers = currentPullReviewers
|
||||||
}
|
}
|
||||||
|
|
||||||
if canChooseReviewer && reviewersResult != nil {
|
if data.CanChooseReviewer && reviewersResult != nil {
|
||||||
preadded := len(reviewersResult)
|
preadded := len(reviewersResult)
|
||||||
for _, reviewer := range reviewers {
|
for _, reviewer := range reviewers {
|
||||||
found := false
|
found := false
|
||||||
@ -807,10 +943,10 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["Reviewers"] = reviewersResult
|
data.Reviewers = reviewersResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if canChooseReviewer && teamReviewersResult != nil {
|
if data.CanChooseReviewer && teamReviewersResult != nil {
|
||||||
preadded := len(teamReviewersResult)
|
preadded := len(teamReviewersResult)
|
||||||
for _, team := range teamReviewers {
|
for _, team := range teamReviewers {
|
||||||
found := false
|
found := false
|
||||||
@ -835,55 +971,82 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["TeamReviewers"] = teamReviewersResult
|
data.TeamReviewers = teamReviewersResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveRepoMetas find all the meta information of a repository
|
type issueSidebarLabelsData struct {
|
||||||
func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
|
AllLabels []*issues_model.Label
|
||||||
if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
|
RepoLabels []*issues_model.Label
|
||||||
return nil
|
OrgLabels []*issues_model.Label
|
||||||
|
SelectedLabelIDs string
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSelectedStringIDs[KeyType, ItemType comparable](
|
||||||
|
allLabels []*issues_model.Label, candidateKey func(candidate *issues_model.Label) KeyType,
|
||||||
|
selectedItems []ItemType, selectedKey func(selected ItemType) KeyType,
|
||||||
|
) string {
|
||||||
|
selectedIDSet := make(container.Set[string])
|
||||||
|
allLabelMap := map[KeyType]*issues_model.Label{}
|
||||||
|
for _, label := range allLabels {
|
||||||
|
allLabelMap[candidateKey(label)] = label
|
||||||
}
|
}
|
||||||
|
for _, item := range selectedItems {
|
||||||
|
if label, ok := allLabelMap[selectedKey(item)]; ok {
|
||||||
|
label.IsChecked = true
|
||||||
|
selectedIDSet.Add(strconv.FormatInt(label.ID, 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids := selectedIDSet.Values()
|
||||||
|
sort.Strings(ids)
|
||||||
|
return strings.Join(ids, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *issueSidebarLabelsData) SetSelectedLabels(labels []*issues_model.Label) {
|
||||||
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
||||||
|
d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
|
||||||
|
labels, func(label *issues_model.Label) int64 { return label.ID },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *issueSidebarLabelsData) SetSelectedLabelNames(labelNames []string) {
|
||||||
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
||||||
|
d.AllLabels, func(label *issues_model.Label) string { return strings.ToLower(label.Name) },
|
||||||
|
labelNames, strings.ToLower,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
|
||||||
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
||||||
|
d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
|
||||||
|
labelIDs, func(labelID int64) int64 { return labelID },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
|
||||||
|
repo := d.Repository
|
||||||
|
labelsData := d.LabelsData
|
||||||
|
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetLabelsByRepoID", err)
|
ctx.ServerError("GetLabelsByRepoID", err)
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Labels"] = labels
|
labelsData.RepoLabels = labels
|
||||||
|
|
||||||
if repo.Owner.IsOrganization() {
|
if repo.Owner.IsOrganization() {
|
||||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
labelsData.OrgLabels = orgLabels
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
|
||||||
labels = append(labels, orgLabels...)
|
|
||||||
}
|
}
|
||||||
|
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
|
||||||
RetrieveRepoMilestonesAndAssignees(ctx, repo)
|
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
|
||||||
if ctx.Written() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveProjects(ctx, repo)
|
|
||||||
if ctx.Written() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
PrepareBranchList(ctx)
|
|
||||||
if ctx.Written() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains true if the user can create issue dependencies
|
|
||||||
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
|
|
||||||
|
|
||||||
return labels
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
|
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
|
||||||
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
|
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error) {
|
||||||
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -920,43 +1083,21 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
|||||||
ctx.Data["Fields"] = template.Fields
|
ctx.Data["Fields"] = template.Fields
|
||||||
ctx.Data["TemplateFile"] = template.FileName
|
ctx.Data["TemplateFile"] = template.FileName
|
||||||
}
|
}
|
||||||
labelIDs := make([]string, 0, len(template.Labels))
|
|
||||||
if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
|
|
||||||
ctx.Data["Labels"] = repoLabels
|
|
||||||
if ctx.Repo.Owner.IsOrganization() {
|
|
||||||
if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
|
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
|
||||||
repoLabels = append(repoLabels, orgLabels...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, metaLabel := range template.Labels {
|
metaData.LabelsData.SetSelectedLabelNames(template.Labels)
|
||||||
for _, repoLabel := range repoLabels {
|
|
||||||
if strings.EqualFold(repoLabel.Name, metaLabel) {
|
|
||||||
repoLabel.IsChecked = true
|
|
||||||
labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
|
|
||||||
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
|
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
|
||||||
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
|
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, true); err == nil {
|
||||||
for _, userID := range userIDs {
|
for _, userID := range userIDs {
|
||||||
selectedAssigneeIDs = append(selectedAssigneeIDs, userID)
|
|
||||||
selectedAssigneeIDStrings = append(selectedAssigneeIDStrings, strconv.FormatInt(userID, 10))
|
selectedAssigneeIDStrings = append(selectedAssigneeIDStrings, strconv.FormatInt(userID, 10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
metaData.AssigneesData.SelectedAssigneeIDs = strings.Join(selectedAssigneeIDStrings, ",")
|
||||||
|
|
||||||
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
||||||
template.Ref = git.BranchPrefix + template.Ref
|
template.Ref = git.BranchPrefix + template.Ref
|
||||||
}
|
}
|
||||||
ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
|
|
||||||
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
|
|
||||||
ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
|
|
||||||
ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
|
|
||||||
ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
|
|
||||||
ctx.Data["Reference"] = template.Ref
|
ctx.Data["Reference"] = template.Ref
|
||||||
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
|
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
|
||||||
return true, templateErrs
|
return true, templateErrs
|
||||||
@ -983,36 +1124,19 @@ func NewIssue(ctx *context.Context) {
|
|||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
milestoneID := ctx.FormInt64("milestone")
|
pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, false)
|
||||||
if milestoneID > 0 {
|
if ctx.Written() {
|
||||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
|
return
|
||||||
if err != nil {
|
|
||||||
log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
|
|
||||||
} else {
|
|
||||||
ctx.Data["milestone_id"] = milestoneID
|
|
||||||
ctx.Data["Milestone"] = milestone
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
projectID := ctx.FormInt64("project")
|
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
|
||||||
if projectID > 0 && isProjectsEnabled {
|
pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project")
|
||||||
project, err := project_model.GetProjectByID(ctx, projectID)
|
if pageMetaData.ProjectsData.SelectedProjectID > 0 {
|
||||||
if err != nil {
|
|
||||||
log.Error("GetProjectByID: %d: %v", projectID, err)
|
|
||||||
} else if project.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
|
|
||||||
} else {
|
|
||||||
ctx.Data["project_id"] = projectID
|
|
||||||
ctx.Data["Project"] = project
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ctx.Req.URL.Query().Get("project")) > 0 {
|
if len(ctx.Req.URL.Query().Get("project")) > 0 {
|
||||||
ctx.Data["redirect_after_creation"] = "project"
|
ctx.Data["redirect_after_creation"] = "project"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
|
|
||||||
|
|
||||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||||
@ -1021,7 +1145,7 @@ func NewIssue(ctx *context.Context) {
|
|||||||
ctx.Data["Tags"] = tags
|
ctx.Data["Tags"] = tags
|
||||||
|
|
||||||
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
|
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
|
||||||
for k, v := range errs {
|
for k, v := range errs {
|
||||||
ret.TemplateErrors[k] = v
|
ret.TemplateErrors[k] = v
|
||||||
}
|
}
|
||||||
@ -1116,107 +1240,95 @@ func DeleteIssue(ctx *context.Context) {
|
|||||||
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
|
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRepoMetas check and returns repository's meta information
|
func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(ItemType) KeyType) container.Set[KeyType] {
|
||||||
func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
|
s := make(container.Set[KeyType])
|
||||||
var (
|
for _, item := range slice {
|
||||||
repo = ctx.Repo.Repository
|
s.Add(keyFunc(item))
|
||||||
err error
|
}
|
||||||
)
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
|
// ValidateRepoMetasForNewIssue check and returns repository's meta information
|
||||||
|
func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
|
||||||
|
LabelIDs, AssigneeIDs []int64
|
||||||
|
MilestoneID, ProjectID int64
|
||||||
|
|
||||||
|
Reviewers []*user_model.User
|
||||||
|
TeamReviewers []*organization.Team
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, isPull)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return nil, nil, 0, 0
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []int64
|
inputLabelIDs, _ := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
||||||
hasSelected := false
|
candidateLabels := toSet(pageMetaData.LabelsData.AllLabels, func(label *issues_model.Label) int64 { return label.ID })
|
||||||
// Check labels.
|
if len(inputLabelIDs) > 0 && !candidateLabels.Contains(inputLabelIDs...) {
|
||||||
if len(form.LabelIDs) > 0 {
|
ctx.NotFound("", nil)
|
||||||
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
return ret
|
||||||
if err != nil {
|
}
|
||||||
return nil, nil, 0, 0
|
pageMetaData.LabelsData.SetSelectedLabelIDs(inputLabelIDs)
|
||||||
}
|
|
||||||
labelIDMark := make(container.Set[int64])
|
|
||||||
labelIDMark.AddMultiple(labelIDs...)
|
|
||||||
|
|
||||||
for i := range labels {
|
allMilestones := append(slices.Clone(pageMetaData.MilestonesData.OpenMilestones), pageMetaData.MilestonesData.ClosedMilestones...)
|
||||||
if labelIDMark.Contains(labels[i].ID) {
|
candidateMilestones := toSet(allMilestones, func(milestone *issues_model.Milestone) int64 { return milestone.ID })
|
||||||
labels[i].IsChecked = true
|
if form.MilestoneID > 0 && !candidateMilestones.Contains(form.MilestoneID) {
|
||||||
hasSelected = true
|
ctx.NotFound("", nil)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
pageMetaData.MilestonesData.SelectedMilestoneID = form.MilestoneID
|
||||||
|
|
||||||
|
allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
|
||||||
|
candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
|
||||||
|
if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) {
|
||||||
|
ctx.NotFound("", nil)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
|
||||||
|
|
||||||
|
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
|
||||||
|
inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
|
||||||
|
if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) {
|
||||||
|
ctx.NotFound("", nil)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs
|
||||||
|
|
||||||
|
// Check if the passed reviewers (user/team) actually exist
|
||||||
|
var reviewers []*user_model.User
|
||||||
|
var teamReviewers []*organization.Team
|
||||||
|
reviewerIDs, _ := base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
|
||||||
|
if isPull && len(reviewerIDs) > 0 {
|
||||||
|
userReviewersMap := map[int64]*user_model.User{}
|
||||||
|
teamReviewersMap := map[int64]*organization.Team{}
|
||||||
|
for _, r := range pageMetaData.ReviewersData.Reviewers {
|
||||||
|
userReviewersMap[r.User.ID] = r.User
|
||||||
|
}
|
||||||
|
for _, r := range pageMetaData.ReviewersData.TeamReviewers {
|
||||||
|
teamReviewersMap[r.Team.ID] = r.Team
|
||||||
|
}
|
||||||
|
for _, rID := range reviewerIDs {
|
||||||
|
if rID < 0 { // negative reviewIDs represent team requests
|
||||||
|
team, ok := teamReviewersMap[-rID]
|
||||||
|
if !ok {
|
||||||
|
ctx.NotFound("", nil)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
teamReviewers = append(teamReviewers, team)
|
||||||
|
} else {
|
||||||
|
user, ok := userReviewersMap[rID]
|
||||||
|
if !ok {
|
||||||
|
ctx.NotFound("", nil)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
reviewers = append(reviewers, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["Labels"] = labels
|
ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, form.ProjectID
|
||||||
ctx.Data["HasSelectedLabel"] = hasSelected
|
ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers
|
||||||
ctx.Data["label_ids"] = form.LabelIDs
|
return ret
|
||||||
|
|
||||||
// Check milestone.
|
|
||||||
milestoneID := form.MilestoneID
|
|
||||||
if milestoneID > 0 {
|
|
||||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetMilestoneByID", err)
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
if milestone.RepoID != repo.ID {
|
|
||||||
ctx.ServerError("GetMilestoneByID", err)
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
ctx.Data["Milestone"] = milestone
|
|
||||||
ctx.Data["milestone_id"] = milestoneID
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.ProjectID > 0 {
|
|
||||||
p, err := project_model.GetProjectByID(ctx, form.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetProjectByID", err)
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
|
|
||||||
ctx.NotFound("", nil)
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["Project"] = p
|
|
||||||
ctx.Data["project_id"] = form.ProjectID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check assignees
|
|
||||||
var assigneeIDs []int64
|
|
||||||
if len(form.AssigneeIDs) > 0 {
|
|
||||||
assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the passed assignees actually exists and is assignable
|
|
||||||
for _, aID := range assigneeIDs {
|
|
||||||
assignee, err := user_model.GetUserByID(ctx, aID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetUserByID", err)
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("CanBeAssigned", err)
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if !valid {
|
|
||||||
ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
|
|
||||||
return nil, nil, 0, 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the old assignee id thingy for compatibility reasons
|
|
||||||
if form.AssigneeID > 0 {
|
|
||||||
assigneeIDs = append(assigneeIDs, form.AssigneeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssuePost response for creating new issue
|
// NewIssuePost response for creating new issue
|
||||||
@ -1234,11 +1346,13 @@ func NewIssuePost(ctx *context.Context) {
|
|||||||
attachments []string
|
attachments []string
|
||||||
)
|
)
|
||||||
|
|
||||||
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
|
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
|
||||||
|
|
||||||
if projectID > 0 {
|
if projectID > 0 {
|
||||||
if !ctx.Repo.CanRead(unit.TypeProjects) {
|
if !ctx.Repo.CanRead(unit.TypeProjects) {
|
||||||
// User must also be able to see the project.
|
// User must also be able to see the project.
|
||||||
@ -1507,60 +1621,11 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metas.
|
pageMetaData := retrieveRepoIssueMetaData(ctx, repo, issue, issue.IsPull)
|
||||||
// Check labels.
|
if ctx.Written() {
|
||||||
labelIDMark := make(container.Set[int64])
|
|
||||||
for _, label := range issue.Labels {
|
|
||||||
labelIDMark.Add(label.ID)
|
|
||||||
}
|
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetLabelsByRepoID", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Labels"] = labels
|
pageMetaData.LabelsData.SetSelectedLabels(issue.Labels)
|
||||||
|
|
||||||
if repo.Owner.IsOrganization() {
|
|
||||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetLabelsByOrgID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
|
||||||
|
|
||||||
labels = append(labels, orgLabels...)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSelected := false
|
|
||||||
for i := range labels {
|
|
||||||
if labelIDMark.Contains(labels[i].ID) {
|
|
||||||
labels[i].IsChecked = true
|
|
||||||
hasSelected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.Data["HasSelectedLabel"] = hasSelected
|
|
||||||
|
|
||||||
// Check milestone and assignee.
|
|
||||||
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
||||||
RetrieveRepoMilestonesAndAssignees(ctx, repo)
|
|
||||||
retrieveProjects(ctx, repo)
|
|
||||||
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if issue.IsPull {
|
|
||||||
canChooseReviewer := false
|
|
||||||
if ctx.Doer != nil && ctx.IsSigned {
|
|
||||||
canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
|
|
||||||
}
|
|
||||||
|
|
||||||
RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
// Update issue-user.
|
// Update issue-user.
|
||||||
@ -2479,7 +2544,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue)
|
_, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if issues_model.IsErrNotValidReviewRequest(err) {
|
if issues_model.IsErrNotValidReviewRequest(err) {
|
||||||
log.Warn(
|
log.Warn(
|
||||||
@ -2490,12 +2555,6 @@ func UpdatePullReviewRequest(ctx *context.Context) {
|
|||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.ServerError("IsValidTeamReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("TeamReviewRequest", err)
|
ctx.ServerError("TeamReviewRequest", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2517,7 +2576,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil)
|
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, &ctx.Repo.Permission, reviewer, action == "attach")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if issues_model.IsErrNotValidReviewRequest(err) {
|
if issues_model.IsErrNotValidReviewRequest(err) {
|
||||||
log.Warn(
|
log.Warn(
|
||||||
@ -2528,12 +2587,6 @@ func UpdatePullReviewRequest(ctx *context.Context) {
|
|||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.ServerError("isValidReviewRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
|
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrReviewRequestOnClosedPR(err) {
|
if issues_model.IsErrReviewRequestOnClosedPR(err) {
|
||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -53,11 +53,11 @@ func InitializeLabels(ctx *context.Context) {
|
|||||||
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveLabels find all the labels of a repository and organization
|
// RetrieveLabelsForList find all the labels of a repository and organization, it is only used by "/labels" page to list all labels
|
||||||
func RetrieveLabels(ctx *context.Context) {
|
func RetrieveLabelsForList(ctx *context.Context) {
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
|
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("RetrieveLabels.GetLabels", err)
|
ctx.ServerError("RetrieveLabelsForList.GetLabels", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) {
|
|||||||
contexttest.LoadUser(t, ctx, 2)
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
contexttest.LoadRepo(t, ctx, testCase.RepoID)
|
contexttest.LoadRepo(t, ctx, testCase.RepoID)
|
||||||
ctx.Req.Form.Set("sort", testCase.Sort)
|
ctx.Req.Form.Set("sort", testCase.Sort)
|
||||||
RetrieveLabels(ctx)
|
RetrieveLabelsForList(ctx)
|
||||||
assert.False(t, ctx.Written())
|
assert.False(t, ctx.Written())
|
||||||
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
|
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|||||||
@ -1269,11 +1269,13 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true)
|
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
|
||||||
|
|
||||||
if setting.Attachment.Enabled {
|
if setting.Attachment.Enabled {
|
||||||
attachments = form.Files
|
attachments = form.Files
|
||||||
}
|
}
|
||||||
@ -1318,8 +1320,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
|
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
|
||||||
// instead of 500.
|
// instead of 500.
|
||||||
|
prOpts := &pull_service.NewPullRequestOptions{
|
||||||
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
|
Repo: repo,
|
||||||
|
Issue: pullIssue,
|
||||||
|
LabelIDs: labelIDs,
|
||||||
|
AttachmentUUIDs: attachments,
|
||||||
|
PullRequest: pullRequest,
|
||||||
|
AssigneeIDs: assigneeIDs,
|
||||||
|
Reviewers: validateRet.Reviewers,
|
||||||
|
TeamReviewers: validateRet.TeamReviewers,
|
||||||
|
}
|
||||||
|
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||||
switch {
|
switch {
|
||||||
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
|
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
|
||||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||||
|
|||||||
@ -1163,7 +1163,7 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
|
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
|
||||||
m.Get("/pulls/posters", repo.PullPosters)
|
m.Get("/pulls/posters", repo.PullPosters)
|
||||||
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
|
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
|
||||||
m.Get("/labels", repo.RetrieveLabels, repo.Labels)
|
m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
|
||||||
m.Get("/milestones", repo.Milestones)
|
m.Get("/milestones", repo.Milestones)
|
||||||
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
|
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
|
||||||
m.Group("/{type:issues|pulls}", func() {
|
m.Group("/{type:issues|pulls}", func() {
|
||||||
|
|||||||
@ -137,8 +137,12 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
|
|||||||
Type: issues_model.PullRequestGitea,
|
Type: issues_model.PullRequestGitea,
|
||||||
Flow: issues_model.PullRequestFlowAGit,
|
Flow: issues_model.PullRequestFlowAGit,
|
||||||
}
|
}
|
||||||
|
prOpts := &pull_service.NewPullRequestOptions{
|
||||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
|
Repo: repo,
|
||||||
|
Issue: prIssue,
|
||||||
|
PullRequest: pr,
|
||||||
|
}
|
||||||
|
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,6 @@ type Organization struct {
|
|||||||
Organization *organization.Organization
|
Organization *organization.Organization
|
||||||
OrgLink string
|
OrgLink string
|
||||||
CanCreateOrgRepo bool
|
CanCreateOrgRepo bool
|
||||||
PublicMemberOnly bool // Only display public members
|
|
||||||
|
|
||||||
Team *organization.Team
|
Team *organization.Team
|
||||||
Teams []*organization.Team
|
Teams []*organization.Team
|
||||||
@ -176,10 +175,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
|||||||
ctx.Data["OrgLink"] = ctx.Org.OrgLink
|
ctx.Data["OrgLink"] = ctx.Org.OrgLink
|
||||||
|
|
||||||
// Member
|
// Member
|
||||||
ctx.Org.PublicMemberOnly = ctx.Doer == nil || !ctx.Org.IsMember && !ctx.Doer.IsAdmin
|
|
||||||
opts := &organization.FindOrgMembersOpts{
|
opts := &organization.FindOrgMembersOpts{
|
||||||
OrgID: org.ID,
|
Doer: ctx.Doer,
|
||||||
PublicOnly: ctx.Org.PublicMemberOnly,
|
OrgID: org.ID,
|
||||||
|
IsDoerMember: ctx.Org.IsMember,
|
||||||
}
|
}
|
||||||
ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts)
|
ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
70
services/doctor/actions.go
Normal file
70
services/doctor/actions.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||||
|
var reposToFix []*repo_model.Repository
|
||||||
|
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
repos, _, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
PageSize: repo_model.RepositoryListDefaultPageSize,
|
||||||
|
Page: page,
|
||||||
|
},
|
||||||
|
Mirror: optional.Some(true),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SearchRepository: %w", err)
|
||||||
|
}
|
||||||
|
if len(repos) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range repos {
|
||||||
|
if repo.UnitEnabled(ctx, unit_model.TypeActions) {
|
||||||
|
reposToFix = append(reposToFix, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reposToFix) == 0 {
|
||||||
|
logger.Info("Found no mirror with actions unit enabled")
|
||||||
|
} else {
|
||||||
|
logger.Warn("Found %d mirrors with actions unit enabled", len(reposToFix))
|
||||||
|
}
|
||||||
|
if !autofix || len(reposToFix) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range reposToFix {
|
||||||
|
if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Info("Fixed %d mirrors with actions unit enabled", len(reposToFix))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Check{
|
||||||
|
Title: "Disable the actions unit for all mirrors",
|
||||||
|
Name: "disable-mirror-actions-unit",
|
||||||
|
IsDefault: false,
|
||||||
|
Run: disableMirrorActionsUnit,
|
||||||
|
Priority: 9,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -447,10 +447,10 @@ type CreateIssueForm struct {
|
|||||||
Title string `binding:"Required;MaxSize(255)"`
|
Title string `binding:"Required;MaxSize(255)"`
|
||||||
LabelIDs string `form:"label_ids"`
|
LabelIDs string `form:"label_ids"`
|
||||||
AssigneeIDs string `form:"assignee_ids"`
|
AssigneeIDs string `form:"assignee_ids"`
|
||||||
|
ReviewerIDs string `form:"reviewer_ids"`
|
||||||
Ref string `form:"ref"`
|
Ref string `form:"ref"`
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
ProjectID int64
|
ProjectID int64
|
||||||
AssigneeID int64
|
|
||||||
Content string
|
Content string
|
||||||
Files []string
|
Files []string
|
||||||
AllowMaintainerEdit bool
|
AllowMaintainerEdit bool
|
||||||
|
|||||||
@ -61,7 +61,12 @@ func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, do
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
|
// ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
|
||||||
func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
|
func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, permDoer *access_model.Permission, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||||
|
err = isValidReviewRequest(ctx, reviewer, doer, isAdd, issue, permDoer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if isAdd {
|
if isAdd {
|
||||||
comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer)
|
comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer)
|
||||||
} else {
|
} else {
|
||||||
@ -79,8 +84,8 @@ func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewe
|
|||||||
return comment, err
|
return comment, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidReviewRequest Check permission for ReviewRequest
|
// isValidReviewRequest Check permission for ReviewRequest
|
||||||
func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
|
func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
|
||||||
if reviewer.IsOrganization() {
|
if reviewer.IsOrganization() {
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
Reason: "Organization can't be added as reviewer",
|
Reason: "Organization can't be added as reviewer",
|
||||||
@ -109,7 +114,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastreview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
||||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -137,7 +142,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
|
if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastReview != nil && lastReview.Type != issues_model.ReviewTypeRequest {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +157,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
|
if lastReview != nil && lastReview.Type == issues_model.ReviewTypeRequest && lastReview.ReviewerID == doer.ID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,8 +168,8 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidTeamReviewRequest Check permission for ReviewRequest Team
|
// isValidTeamReviewRequest Check permission for ReviewRequest Team
|
||||||
func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
|
func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
|
||||||
if doer.IsOrganization() {
|
if doer.IsOrganization() {
|
||||||
return issues_model.ErrNotValidReviewRequest{
|
return issues_model.ErrNotValidReviewRequest{
|
||||||
Reason: "Organization can't be doer to add reviewer",
|
Reason: "Organization can't be doer to add reviewer",
|
||||||
@ -212,6 +217,10 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
|
|||||||
|
|
||||||
// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
|
// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
|
||||||
func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
|
func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
|
||||||
|
err = isValidTeamReviewRequest(ctx, reviewer, doer, isAdd, issue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if isAdd {
|
if isAdd {
|
||||||
comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer)
|
comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer)
|
||||||
} else {
|
} else {
|
||||||
@ -268,6 +277,9 @@ func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doe
|
|||||||
|
|
||||||
// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
|
// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
|
||||||
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
|
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
|
||||||
|
if repo.IsArchived {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// The poster of the PR can change the reviewers
|
// The poster of the PR can change the reviewers
|
||||||
if doer.ID == issue.PosterID {
|
if doer.ID == issue.PosterID {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -382,7 +382,7 @@ func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error {
|
|||||||
if _, err := msg.WriteTo(&buf); err != nil {
|
if _, err := msg.WriteTo(&buf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Info("Mail From: %s To: %v Body: %s", from, to, buf.String())
|
log.Debug("Mail From: %s To: %v Body: %s", from, to, buf.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
@ -41,8 +42,20 @@ func getPullWorkingLockKey(prID int64) string {
|
|||||||
return fmt.Sprintf("pull_working_%d", prID)
|
return fmt.Sprintf("pull_working_%d", prID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NewPullRequestOptions struct {
|
||||||
|
Repo *repo_model.Repository
|
||||||
|
Issue *issues_model.Issue
|
||||||
|
LabelIDs []int64
|
||||||
|
AttachmentUUIDs []string
|
||||||
|
PullRequest *issues_model.PullRequest
|
||||||
|
AssigneeIDs []int64
|
||||||
|
Reviewers []*user_model.User
|
||||||
|
TeamReviewers []*organization.Team
|
||||||
|
}
|
||||||
|
|
||||||
// NewPullRequest creates new pull request with labels for repository.
|
// NewPullRequest creates new pull request with labels for repository.
|
||||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
|
func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
||||||
|
repo, issue, labelIDs, uuids, pr, assigneeIDs := opts.Repo, opts.Issue, opts.LabelIDs, opts.AttachmentUUIDs, opts.PullRequest, opts.AssigneeIDs
|
||||||
if err := issue.LoadPoster(ctx); err != nil {
|
if err := issue.LoadPoster(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -197,7 +210,17 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
|
|||||||
}
|
}
|
||||||
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
|
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
|
||||||
}
|
}
|
||||||
|
permDoer, err := access_model.GetUserRepoPermission(ctx, repo, issue.Poster)
|
||||||
|
for _, reviewer := range opts.Reviewers {
|
||||||
|
if _, err = issue_service.ReviewRequest(ctx, pr.Issue, issue.Poster, &permDoer, reviewer, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, teamReviewer := range opts.TeamReviewers {
|
||||||
|
if _, err = issue_service.TeamReviewRequest(ctx, pr.Issue, issue.Poster, teamReviewer, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -381,8 +381,13 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
|||||||
|
|
||||||
// insert units for repo
|
// insert units for repo
|
||||||
defaultUnits := unit.DefaultRepoUnits
|
defaultUnits := unit.DefaultRepoUnits
|
||||||
if isFork {
|
switch {
|
||||||
|
case isFork:
|
||||||
defaultUnits = unit.DefaultForkRepoUnits
|
defaultUnits = unit.DefaultForkRepoUnits
|
||||||
|
case repo.IsMirror:
|
||||||
|
defaultUnits = unit.DefaultMirrorRepoUnits
|
||||||
|
case repo.IsTemplate:
|
||||||
|
defaultUnits = unit.DefaultTemplateRepoUnits
|
||||||
}
|
}
|
||||||
units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
|
units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
|
||||||
for _, tp := range defaultUnits {
|
for _, tp := range defaultUnits {
|
||||||
|
|||||||
@ -44,4 +44,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
<a
|
|
||||||
class="item {{if not .label.IsChecked}}tw-hidden{{end}}"
|
|
||||||
id="label_{{.label.ID}}"
|
|
||||||
href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}}
|
|
||||||
>
|
|
||||||
{{- ctx.RenderUtils.RenderLabel .label -}}
|
|
||||||
</a>
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-label dropdown">
|
|
||||||
<span class="text muted flex-text-block">
|
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong>
|
|
||||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
<div class="filter menu" {{if .Issue}}data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels"{{else}}data-id="#label_ids"{{end}}>
|
|
||||||
{{if or .Labels .OrgLabels}}
|
|
||||||
<div class="ui icon search input">
|
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<a class="no-select item" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
|
|
||||||
{{if or .Labels .OrgLabels}}
|
|
||||||
{{$previousExclusiveScope := "_no_scope"}}
|
|
||||||
{{range .Labels}}
|
|
||||||
{{$exclusiveScope := .ExclusiveScope}}
|
|
||||||
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{end}}
|
|
||||||
{{$previousExclusiveScope = $exclusiveScope}}
|
|
||||||
<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</span> {{ctx.RenderUtils.RenderLabel .}}
|
|
||||||
{{if .Description}}<br><small class="desc">{{.Description | ctx.RenderUtils.RenderEmoji}}</small>{{end}}
|
|
||||||
<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{$previousExclusiveScope = "_no_scope"}}
|
|
||||||
{{range .OrgLabels}}
|
|
||||||
{{$exclusiveScope := .ExclusiveScope}}
|
|
||||||
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{end}}
|
|
||||||
{{$previousExclusiveScope = $exclusiveScope}}
|
|
||||||
<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</span> {{ctx.RenderUtils.RenderLabel .}}
|
|
||||||
{{if .Description}}<br><small class="desc">{{.Description | ctx.RenderUtils.RenderEmoji}}</small>{{end}}
|
|
||||||
<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<div class="disabled item">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<div class="ui labels list">
|
|
||||||
<span class="labels-list">
|
|
||||||
<span class="no-select {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
|
|
||||||
{{range .root.Labels}}
|
|
||||||
{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
|
|
||||||
{{end}}
|
|
||||||
{{range .root.OrgLabels}}
|
|
||||||
{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{{if or .OpenMilestones .ClosedMilestones}}
|
|
||||||
<div class="ui icon search input">
|
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestones"}}">
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{end}}
|
|
||||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
|
|
||||||
{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
|
|
||||||
<div class="disabled item">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.no_items"}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{if .OpenMilestones}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="header">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
|
|
||||||
</div>
|
|
||||||
{{range .OpenMilestones}}
|
|
||||||
<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
|
|
||||||
{{svg "octicon-milestone" 16 "tw-mr-1"}}
|
|
||||||
{{.Name}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{if .ClosedMilestones}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="header">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}
|
|
||||||
</div>
|
|
||||||
{{range .ClosedMilestones}}
|
|
||||||
<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
|
|
||||||
{{svg "octicon-milestone" 16 "tw-mr-1"}}
|
|
||||||
{{.Name}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
@ -18,15 +18,15 @@
|
|||||||
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
|
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
|
||||||
{{range .Fields}}
|
{{range .Fields}}
|
||||||
{{if eq .Type "input"}}
|
{{if eq .Type "input"}}
|
||||||
{{template "repo/issue/fields/input" "item" .}}
|
{{template "repo/issue/fields/input" dict "item" .}}
|
||||||
{{else if eq .Type "markdown"}}
|
{{else if eq .Type "markdown"}}
|
||||||
{{template "repo/issue/fields/markdown" "item" .}}
|
{{template "repo/issue/fields/markdown" dict "item" .}}
|
||||||
{{else if eq .Type "textarea"}}
|
{{else if eq .Type "textarea"}}
|
||||||
{{template "repo/issue/fields/textarea" "item" . "root" $}}
|
{{template "repo/issue/fields/textarea" dict "item" . "root" $}}
|
||||||
{{else if eq .Type "dropdown"}}
|
{{else if eq .Type "dropdown"}}
|
||||||
{{template "repo/issue/fields/dropdown" "item" .}}
|
{{template "repo/issue/fields/dropdown" dict "item" .}}
|
||||||
{{else if eq .Type "checkboxes"}}
|
{{else if eq .Type "checkboxes"}}
|
||||||
{{template "repo/issue/fields/checkboxes" "item" .}}
|
{{template "repo/issue/fields/checkboxes" dict "item" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -47,142 +47,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="issue-content-right ui segment">
|
<div class="issue-content-right ui segment">
|
||||||
{{template "repo/issue/branch_selector_field" .}}
|
{{template "repo/issue/branch_selector_field" $}}
|
||||||
|
{{if .PageIsComparePull}}
|
||||||
<input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}">
|
{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
|
||||||
{{template "repo/issue/labels/labels_selector_field" .}}
|
<div class="divider"></div>
|
||||||
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<input id="milestone_id" name="milestone_id" type="hidden" value="{{.milestone_id}}">
|
|
||||||
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating jump select-milestone dropdown">
|
|
||||||
<span class="text flex-text-block">
|
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
|
|
||||||
{{if .HasIssuesOrPullsWritePermission}}
|
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
<div class="menu">
|
|
||||||
{{template "repo/issue/milestone/select_menu" .}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui select-milestone list">
|
|
||||||
<span class="no-select item {{if .Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
|
|
||||||
<div class="selected">
|
|
||||||
{{if .Milestone}}
|
|
||||||
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/issues?milestone={{.Milestone.ID}}">
|
|
||||||
{{svg "octicon-milestone" 18 "tw-mr-2"}}
|
|
||||||
{{.Milestone.Name}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .IsProjectsEnabled}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<input id="project_id" name="project_id" type="hidden" value="{{.project_id}}">
|
|
||||||
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating jump select-project dropdown">
|
|
||||||
<span class="text flex-text-block">
|
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
|
|
||||||
{{if .HasIssuesOrPullsWritePermission}}
|
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
<div class="menu">
|
|
||||||
{{if or .OpenProjects .ClosedProjects}}
|
|
||||||
<div class="ui icon search input">
|
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_projects"}}">
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
|
|
||||||
{{if and (not .OpenProjects) (not .ClosedProjects)}}
|
|
||||||
<div class="disabled item">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.no_items"}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{if .OpenProjects}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="header">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
|
|
||||||
</div>
|
|
||||||
{{range .OpenProjects}}
|
|
||||||
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
|
|
||||||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{if .ClosedProjects}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="header">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
|
|
||||||
</div>
|
|
||||||
{{range .ClosedProjects}}
|
|
||||||
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
|
|
||||||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui select-project list">
|
|
||||||
<span class="no-select item {{if .Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
|
|
||||||
<div class="selected">
|
|
||||||
{{if .Project}}
|
|
||||||
<a class="item muted sidebar-item-link" href="{{.Project.Link ctx}}">
|
|
||||||
{{svg .Project.IconName 18 "tw-mr-2"}}{{.Project.Title}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="divider"></div>
|
|
||||||
<input id="assignee_ids" name="assignee_ids" type="hidden" value="{{.assignee_ids}}">
|
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
|
||||||
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating jump select-assignees dropdown">
|
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
|
||||||
<span class="text flex-text-block">
|
{{if .IsProjectsEnabled}}
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
|
{{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}
|
||||||
{{if .HasIssuesOrPullsWritePermission}}
|
{{end}}
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
<div class="filter menu" data-id="#assignee_ids">
|
|
||||||
<div class="ui icon search input">
|
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
|
|
||||||
</div>
|
|
||||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
|
|
||||||
{{range .Assignees}}
|
|
||||||
<a class="{{if SliceUtils.Contains $.SelectedAssigneeIDs .ID}}checked{{end}} item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
|
||||||
<span class="octicon-check {{if not (SliceUtils.Contains $.SelectedAssigneeIDs .ID)}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
|
|
||||||
<span class="text">
|
|
||||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui assignees list">
|
|
||||||
<span class="no-select item {{if .HasSelectedAssignee}}tw-hidden{{end}}">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
|
|
||||||
</span>
|
|
||||||
<div class="selected">
|
|
||||||
{{range .Assignees}}
|
|
||||||
<a class="item tw-p-1 muted {{if not (SliceUtils.Contains $.SelectedAssigneeIDs .ID)}}tw-hidden{{end}}" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
|
|
||||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}
|
{{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="inline field">
|
<div class="ui checkbox">
|
||||||
<div class="ui checkbox">
|
<label data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
|
||||||
<label data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
|
<input name="allow_maintainer_edit" type="checkbox" {{if .AllowMaintainerEdit}}checked{{end}}>
|
||||||
<input name="allow_maintainer_edit" type="checkbox" {{if .AllowMaintainerEdit}}checked{{end}}>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
|
{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
|
||||||
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
|
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="inline field">
|
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
|
||||||
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
|
data-url="{{.Issue.Link}}"
|
||||||
data-url="{{.Issue.Link}}"
|
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
|
||||||
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
|
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
|
||||||
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
|
>
|
||||||
>
|
<label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
|
||||||
<label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
|
<input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}}>
|
||||||
<input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}}>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -1,46 +1,35 @@
|
|||||||
|
{{$pageMeta := .}}
|
||||||
|
{{$data := .AssigneesData}}
|
||||||
|
{{$issueAssignees := NIL}}{{if $pageMeta.Issue}}{{$issueAssignees = $pageMeta.Issue.Assignees}}{{end}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
|
<div class="issue-sidebar-combo" data-selection-mode="multiple" data-update-algo="diff"
|
||||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-assignees-modify dropdown">
|
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/assignee?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||||
<a class="text muted flex-text-block">
|
>
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
|
<input class="combo-value" name="assignee_ids" type="hidden" value="{{$data.SelectedAssigneeIDs}}">
|
||||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
<a class="text muted">
|
||||||
{{end}}
|
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
||||||
</a>
|
</a>
|
||||||
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
|
<div class="menu">
|
||||||
<div class="ui icon search input">
|
<div class="ui icon search input">
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
|
||||||
|
</div>
|
||||||
|
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
|
||||||
|
{{range $data.CandidateAssignees}}
|
||||||
|
<a class="item muted" href="#" data-value="{{.ID}}">
|
||||||
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
|
{{ctx.AvatarUtils.Avatar . 20}} {{template "repo/search_name" .}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
|
</div>
|
||||||
{{range .Assignees}}
|
<div class="ui list tw-flex tw-flex-row tw-gap-2">
|
||||||
|
<span class="item empty-list {{if $issueAssignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
|
||||||
{{$AssigneeID := .ID}}
|
{{range $issueAssignees}}
|
||||||
<a class="item{{range $.Issue.Assignees}}{{if eq .ID $AssigneeID}} checked{{end}}{{end}}" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
<a class="item muted" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
|
||||||
{{$checked := false}}
|
{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
|
||||||
{{range $.Issue.Assignees}}
|
|
||||||
{{if eq .ID $AssigneeID}}
|
|
||||||
{{$checked = true}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
<span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
|
|
||||||
<span class="text">
|
|
||||||
{{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui assignees list">
|
|
||||||
<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
|
|
||||||
<div class="selected">
|
|
||||||
{{range .Issue.Assignees}}
|
|
||||||
<div class="item">
|
|
||||||
<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
|
|
||||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
|
|
||||||
{{.GetDisplayName}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -18,11 +18,11 @@
|
|||||||
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
|
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
|
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
|
||||||
</span>
|
</span>
|
||||||
<div class="ui relaxed divided list">
|
<div class="ui divided list">
|
||||||
{{range .BlockingDependencies}}
|
{{range .BlockingDependencies}}
|
||||||
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
|
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
|
||||||
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
|
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
|
||||||
<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
|
<a class="muted gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
|
||||||
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
|
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
|
||||||
</a>
|
</a>
|
||||||
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
|
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
|
||||||
@ -50,11 +50,11 @@
|
|||||||
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
|
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{ctx.Locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{ctx.Locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
|
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
|
||||||
</span>
|
</span>
|
||||||
<div class="ui relaxed divided list">
|
<div class="ui divided list">
|
||||||
{{range .BlockedByDependencies}}
|
{{range .BlockedByDependencies}}
|
||||||
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
|
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
|
||||||
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
|
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
|
||||||
<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
|
<a class="muted gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
|
||||||
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
|
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
|
||||||
</a>
|
</a>
|
||||||
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
|
<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
|
||||||
@ -76,7 +76,7 @@
|
|||||||
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
|
<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
|
||||||
<div class="gt-ellipsis">
|
<div class="gt-ellipsis">
|
||||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
|
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
|
||||||
<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
|
<span class="gt-ellipsis" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
|
||||||
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
|
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
53
templates/repo/issue/sidebar/label_list.tmpl
Normal file
53
templates/repo/issue/sidebar/label_list.tmpl
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{{$pageMeta := .}}
|
||||||
|
{{$data := .LabelsData}}
|
||||||
|
<div class="issue-sidebar-combo" data-selection-mode="multiple" data-update-algo="diff"
|
||||||
|
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/labels?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||||
|
>
|
||||||
|
<input class="combo-value" name="label_ids" type="hidden" value="{{$data.SelectedLabelIDs}}">
|
||||||
|
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
|
||||||
|
<a class="text muted">
|
||||||
|
<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
||||||
|
</a>
|
||||||
|
<div class="menu">
|
||||||
|
{{if not $data.AllLabels}}
|
||||||
|
<div class="item disabled">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
|
||||||
|
</div>
|
||||||
|
<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
|
||||||
|
{{$previousExclusiveScope := "_no_scope"}}
|
||||||
|
{{range $data.RepoLabels}}
|
||||||
|
{{$exclusiveScope := .ExclusiveScope}}
|
||||||
|
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{end}}
|
||||||
|
{{$previousExclusiveScope = $exclusiveScope}}
|
||||||
|
{{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
|
||||||
|
{{end}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{$previousExclusiveScope = "_no_scope"}}
|
||||||
|
{{range $data.OrgLabels}}
|
||||||
|
{{$exclusiveScope := .ExclusiveScope}}
|
||||||
|
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{end}}
|
||||||
|
{{$previousExclusiveScope = $exclusiveScope}}
|
||||||
|
{{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui list labels-list tw-my-2 tw-flex tw-gap-2">
|
||||||
|
<span class="item empty-list {{if $data.SelectedLabelIDs}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
|
||||||
|
{{range $data.AllLabels}}
|
||||||
|
{{if .IsChecked}}
|
||||||
|
<a class="item" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?labels={{.ID}}">
|
||||||
|
{{- ctx.RenderUtils.RenderLabel . -}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
templates/repo/issue/sidebar/label_list_item.tmpl
Normal file
11
templates/repo/issue/sidebar/label_list_item.tmpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{$label := .Label}}
|
||||||
|
<a class="item muted {{if $label.IsChecked}}checked{{else if $label.IsArchived}}tw-hidden{{end}}" href="#"
|
||||||
|
data-scope="{{$label.ExclusiveScope}}" data-value="{{$label.ID}}" {{if $label.IsArchived}}data-is-archived{{end}}
|
||||||
|
>
|
||||||
|
<span class="item-check-mark">{{svg (Iif $label.ExclusiveScope "octicon-dot-fill" "octicon-check")}}</span>
|
||||||
|
{{ctx.RenderUtils.RenderLabel $label}}
|
||||||
|
<div class="item-secondary-info">
|
||||||
|
{{if $label.Description}}<div class="tw-pl-[20px]"><small>{{$label.Description | ctx.RenderUtils.RenderEmoji}}</small></div>{{end}}
|
||||||
|
<div class="archived-label-hint">{{template "repo/issue/labels/label_archived" $label}}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@ -1,22 +1,52 @@
|
|||||||
|
{{$pageMeta := .}}
|
||||||
|
{{$data := .MilestonesData}}
|
||||||
|
{{$issueMilestone := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Milestone}}{{$issueMilestone = $pageMeta.Issue.Milestone}}{{end}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-milestone dropdown">
|
<div class="issue-sidebar-combo" data-selection-mode="single" data-update-algo="all"
|
||||||
<a class="text muted flex-text-block">
|
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/milestone?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
|
>
|
||||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
<input class="combo-value" name="milestone_id" type="hidden" value="{{$data.SelectedMilestoneID}}">
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}} ">
|
||||||
{{end}}
|
<a class="text muted">
|
||||||
</a>
|
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
||||||
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
|
</a>
|
||||||
{{template "repo/issue/milestone/select_menu" .}}
|
<div class="menu">
|
||||||
|
{{if and (not $data.OpenMilestones) (not $data.ClosedMilestones)}}
|
||||||
|
<div class="item disabled">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="icon">{{svg "octicon-search"}}</i>
|
||||||
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestones"}}">
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
|
||||||
|
{{if $data.OpenMilestones}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}</div>
|
||||||
|
{{range $data.OpenMilestones}}
|
||||||
|
<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}">
|
||||||
|
{{svg "octicon-milestone" 18}} {{.Name}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if $data.ClosedMilestones}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="header">{{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}</div>
|
||||||
|
{{range $data.ClosedMilestones}}
|
||||||
|
<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}">
|
||||||
|
{{svg "octicon-milestone" 18}} {{.Name}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="ui select-milestone list">
|
<div class="ui list">
|
||||||
<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
|
<span class="item empty-list {{if $issueMilestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
|
||||||
<div class="selected">
|
{{if $issueMilestone}}
|
||||||
{{if .Issue.Milestone}}
|
<a class="item muted" href="{{$pageMeta.RepoLink}}/milestone/{{$issueMilestone.ID}}">
|
||||||
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
|
{{svg "octicon-milestone" 18}} {{$issueMilestone.Name}}
|
||||||
{{svg "octicon-milestone" 18 "tw-mr-2"}}
|
|
||||||
{{.Issue.Milestone.Name}}
|
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="ui list tw-flex tw-flex-wrap">
|
<div class="ui list tw-flex tw-flex-wrap">
|
||||||
{{range .Participants}}
|
{{range .Participants}}
|
||||||
<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
|
<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
|
||||||
{{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}}
|
{{ctx.AvatarUtils.Avatar . 20 "tw-my-0.5 tw-mr-1"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,53 +1,49 @@
|
|||||||
{{if .IsProjectsEnabled}}
|
{{$pageMeta := .}}
|
||||||
<div class="divider"></div>
|
{{$data := .ProjectsData}}
|
||||||
|
{{$issueProject := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Project}}{{$issueProject = $pageMeta.Issue.Project}}{{end}}
|
||||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-project dropdown">
|
<div class="divider"></div>
|
||||||
<a class="text muted flex-text-block">
|
<div class="issue-sidebar-combo" data-selection-mode="single" data-update-algo="all"
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
|
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
>
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
<input class="combo-value" name="project_id" type="hidden" value="{{$data.SelectedProjectID}}">
|
||||||
{{end}}
|
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
|
||||||
|
<a class="text muted">
|
||||||
|
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
||||||
</a>
|
</a>
|
||||||
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects">
|
<div class="menu">
|
||||||
{{if or .OpenProjects .ClosedProjects}}
|
{{if or $data.OpenProjects $data.ClosedProjects}}
|
||||||
<div class="ui icon search input">
|
<div class="ui icon search input">
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_projects"}}">
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_projects"}}">
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
|
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
|
||||||
{{if .OpenProjects}}
|
{{if $data.OpenProjects}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="header">
|
<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_projects"}}</div>
|
||||||
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
|
{{range $data.OpenProjects}}
|
||||||
</div>
|
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
||||||
{{range .OpenProjects}}
|
{{svg .IconName 18}} {{.Title}}
|
||||||
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
|
|
||||||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
|
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .ClosedProjects}}
|
{{if $data.ClosedProjects}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="header">
|
<div class="header">{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}</div>
|
||||||
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
|
{{range $data.ClosedProjects}}
|
||||||
</div>
|
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
||||||
{{range .ClosedProjects}}
|
{{svg .IconName 18}} {{.Title}}
|
||||||
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
|
|
||||||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
|
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui select-project list">
|
<div class="ui list">
|
||||||
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
|
<span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
|
||||||
<div class="selected">
|
{{if $issueProject}}
|
||||||
{{if .Issue.Project}}
|
<a class="item muted" href="{{$issueProject.Link ctx}}">
|
||||||
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
|
{{svg $issueProject.IconName 18}} {{$issueProject.Title}}
|
||||||
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
|
</a>
|
||||||
</a>
|
{{end}}
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
</div>
|
||||||
|
|||||||
@ -1,95 +1,83 @@
|
|||||||
<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
|
{{$pageMeta := .}}
|
||||||
<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
|
{{$data := .ReviewersData}}
|
||||||
<a class="text tw-flex tw-items-center muted">
|
{{$repoOwnerName := $pageMeta.Repository.OwnerName}}
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
|
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
|
||||||
{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
|
<div class="issue-sidebar-combo" data-selection-mode="multiple" data-update-algo="diff"
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/request_review?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||||
{{end}}
|
>
|
||||||
</a>
|
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
|
||||||
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review">
|
<div class="ui dropdown {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
|
||||||
{{if .Reviewers}}
|
<a class="text muted">
|
||||||
<div class="ui icon search input">
|
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
</a>
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_reviewers"}}">
|
<div class="menu flex-items-menu">
|
||||||
</div>
|
{{if $hasCandidates}}
|
||||||
{{end}}
|
<div class="ui icon search input">
|
||||||
{{if .Reviewers}}
|
<i class="icon">{{svg "octicon-search"}}</i>
|
||||||
{{range .Reviewers}}
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_reviewers"}}">
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{range $data.Reviewers}}
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
<a class="item muted {{if .Requested}}checked{{end}}" href="{{.User.HomeLink}}" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
|
||||||
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
|
{{if not .CanChange}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
||||||
<span class="text">
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}}
|
{{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{if $data.TeamReviewers}}
|
||||||
{{if .TeamReviewers}}
|
{{if $data.Reviewers}}<div class="divider"></div>{{end}}
|
||||||
{{if .Reviewers}}
|
{{range $data.TeamReviewers}}
|
||||||
<div class="divider"></div>
|
{{if .Team}}
|
||||||
{{end}}
|
<a class="item muted {{if .Requested}}checked{{end}}" href="#" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
|
||||||
{{range .TeamReviewers}}
|
{{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
||||||
{{if .Team}}
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
|
||||||
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
|
</a>
|
||||||
<span class="text">
|
{{end}}
|
||||||
{{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui assignees list">
|
<div class="ui relaxed list flex-items-block tw-my-4">
|
||||||
<span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
|
<span class="item empty-list {{if or $data.OriginalReviews $data.CurrentPullReviewers}}tw-hidden{{end}}">
|
||||||
<div class="selected">
|
{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}
|
||||||
{{range .PullReviewers}}
|
</span>
|
||||||
<div class="item tw-flex tw-items-center tw-py-2">
|
{{range $data.CurrentPullReviewers}}
|
||||||
<div class="tw-flex tw-items-center tw-flex-1">
|
<div class="item">
|
||||||
|
<div class="flex-text-inline tw-flex-1">
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "tw-mr-2"}}{{.User.GetDisplayName}}</a>
|
<a class="muted flex-text-inline" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}</a>
|
||||||
{{else if .Team}}
|
{{else if .Team}}
|
||||||
<span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
|
{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-flex tw-items-center tw-gap-2">
|
<div class="flex-text-inline">
|
||||||
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
|
{{if .CanBeDismissed}}
|
||||||
<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
|
<a href="#" class="ui muted icon show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}"
|
||||||
|
data-modal="#issue-sidebar-dismiss-review-modal" data-modal-reviewer-id="{{.Review.ID}}">
|
||||||
{{svg "octicon-x" 20}}
|
{{svg "octicon-x" 20}}
|
||||||
</a>
|
</a>
|
||||||
<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
|
|
||||||
<div class="header">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.dismiss_review"}}
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="ui warning message">
|
|
||||||
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
|
|
||||||
</div>
|
|
||||||
<form class="ui form dismiss-review-form" id="dismiss-review-{{.Review.ID}}" action="{{$.RepoLink}}/issues/dismiss_review" method="post">
|
|
||||||
{{$.CsrfTokenHtml}}
|
|
||||||
<input type="hidden" name="review_id" value="{{.Review.ID}}">
|
|
||||||
<div class="field">
|
|
||||||
<label for="message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
|
|
||||||
<input id="message" name="message">
|
|
||||||
</div>
|
|
||||||
<div class="text right actions">
|
|
||||||
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
|
|
||||||
<button class="ui red button" type="submit">{{ctx.Locale.Tr "ok"}}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Review.Stale}}
|
{{if .Review.Stale}}
|
||||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">
|
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">{{svg "octicon-hourglass" 16}}</span>
|
||||||
{{svg "octicon-hourglass" 16}}
|
|
||||||
</span>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
|
{{if and .CanChange $data.CanChooseReviewer}}
|
||||||
<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{svg (Iif .Checked "octicon-trash" "octicon-sync")}}</a>
|
{{if .Requested}}
|
||||||
|
<a href="#" class="ui muted icon link-action"
|
||||||
|
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review"}}"
|
||||||
|
data-url="{{$pageMeta.RepoLink}}/issues/request_review?action=detach&issue_ids={{$pageMeta.Issue.ID}}&id={{.ItemID}}">
|
||||||
|
{{svg "octicon-trash"}}
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="#" class="ui muted icon link-action"
|
||||||
|
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.re_request_review"}}"
|
||||||
|
data-url="{{$pageMeta.RepoLink}}/issues/request_review?action=attach&issue_ids={{$pageMeta.Issue.ID}}&id={{.ItemID}}">
|
||||||
|
{{svg "octicon-sync"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<span {{if .Review.TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .Review.TooltipContent}}"{{end}}>
|
<span {{if .Review.TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .Review.TooltipContent}}"{{end}}>
|
||||||
{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
|
{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
|
||||||
@ -97,15 +85,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range .OriginalReviews}}
|
{{range $data.OriginalReviews}}
|
||||||
<div class="item tw-flex tw-items-center tw-py-2">
|
<div class="item">
|
||||||
<div class="tw-flex tw-items-center tw-flex-1">
|
<div class="flex-text-inline tw-flex-1">
|
||||||
<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
|
{{$originalURLHostname := $pageMeta.Repository.GetOriginalURLHostname}}
|
||||||
{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}}
|
{{$originalURL := $pageMeta.Repository.OriginalURL}}
|
||||||
{{.OriginalAuthor}}
|
<a class="muted flex-text-inline" href="{{$originalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $originalURLHostname}}">
|
||||||
|
{{svg (MigrationIcon $originalURLHostname) 20}} {{.OriginalAuthor}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-flex tw-items-center tw-gap-2">
|
<div class="flex-text-inline">
|
||||||
<span {{if .TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .TooltipContent}}"{{end}}>
|
<span {{if .TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .TooltipContent}}"{{end}}>
|
||||||
{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
|
{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
|
||||||
</span>
|
</span>
|
||||||
@ -113,4 +102,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if $data.CurrentPullReviewers}}
|
||||||
|
<div class="ui small modal" id="issue-sidebar-dismiss-review-modal">
|
||||||
|
<div class="header">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.dismiss_review"}}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="ui warning message">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
|
||||||
|
</div>
|
||||||
|
<form class="ui form" action="{{$pageMeta.RepoLink}}/issues/dismiss_review" method="post">
|
||||||
|
{{ctx.RootData.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" class="reviewer-id" name="review_id">
|
||||||
|
<div class="field">
|
||||||
|
<label for="issue-sidebar-dismiss-review-message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
|
||||||
|
<input id="issue-sidebar-dismiss-review-message" name="message">
|
||||||
|
</div>
|
||||||
|
<div class="text right actions">
|
||||||
|
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
|
||||||
|
<button class="ui red button" type="submit">{{ctx.Locale.Tr "ok"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
|
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
|
||||||
<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
|
<div class="toggle-wip tw-mt-2" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
|
||||||
<a class="muted">
|
<a class="muted">
|
||||||
{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
|
{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -2,17 +2,19 @@
|
|||||||
{{template "repo/issue/branch_selector_field" $}}
|
{{template "repo/issue/branch_selector_field" $}}
|
||||||
|
|
||||||
{{if .Issue.IsPull}}
|
{{if .Issue.IsPull}}
|
||||||
{{template "repo/issue/sidebar/reviewer_list" $}}
|
{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
|
||||||
{{template "repo/issue/sidebar/wip_switch" $}}
|
{{template "repo/issue/sidebar/wip_switch" $}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "repo/issue/labels/labels_selector_field" $}}
|
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
|
||||||
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
|
|
||||||
|
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
|
||||||
|
{{if .IsProjectsEnabled}}
|
||||||
|
{{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}
|
||||||
|
{{end}}
|
||||||
|
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
|
||||||
|
|
||||||
{{template "repo/issue/sidebar/milestone_list" $}}
|
|
||||||
{{template "repo/issue/sidebar/project_list" $}}
|
|
||||||
{{template "repo/issue/sidebar/assignee_list" $}}
|
|
||||||
{{template "repo/issue/sidebar/participant_list" $}}
|
{{template "repo/issue/sidebar/participant_list" $}}
|
||||||
{{template "repo/issue/sidebar/watch_notification" $}}
|
{{template "repo/issue/sidebar/watch_notification" $}}
|
||||||
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}
|
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}
|
||||||
|
|||||||
14
templates/swagger/v1_json.tmpl
generated
14
templates/swagger/v1_json.tmpl
generated
@ -20095,6 +20095,20 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "Milestone"
|
"x-go-name": "Milestone"
|
||||||
},
|
},
|
||||||
|
"reviewers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-go-name": "Reviewers"
|
||||||
|
},
|
||||||
|
"team_reviewers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-go-name": "TeamReviewers"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Title"
|
"x-go-name": "Title"
|
||||||
|
|||||||
@ -145,7 +145,8 @@ func TestPullRequestTargetEvent(t *testing.T) {
|
|||||||
BaseRepo: baseRepo,
|
BaseRepo: baseRepo,
|
||||||
Type: issues_model.PullRequestGitea,
|
Type: issues_model.PullRequestGitea,
|
||||||
}
|
}
|
||||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
||||||
|
err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// load and compare ActionRun
|
// load and compare ActionRun
|
||||||
@ -199,7 +200,8 @@ func TestPullRequestTargetEvent(t *testing.T) {
|
|||||||
BaseRepo: baseRepo,
|
BaseRepo: baseRepo,
|
||||||
Type: issues_model.PullRequestGitea,
|
Type: issues_model.PullRequestGitea,
|
||||||
}
|
}
|
||||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
prOpts = &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
||||||
|
err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// the new pull request cannot trigger actions, so there is still only 1 record
|
// the new pull request cannot trigger actions, so there is still only 1 record
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@ -422,7 +423,9 @@ func TestAPIPullReviewStayDismissed(t *testing.T) {
|
|||||||
pullIssue.ID, user8.ID, 1, 1, 2, false)
|
pullIssue.ID, user8.ID, 1, 1, 2, false)
|
||||||
|
|
||||||
// user8 dismiss review
|
// user8 dismiss review
|
||||||
_, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, user8, false)
|
permUser8, err := access_model.GetUserRepoPermission(db.DefaultContext, pullIssue.Repo, user8)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, &permUser8, user8, false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
reviewsCountCheck(t,
|
reviewsCountCheck(t,
|
||||||
|
|||||||
@ -520,7 +520,8 @@ func TestConflictChecking(t *testing.T) {
|
|||||||
BaseRepo: baseRepo,
|
BaseRepo: baseRepo,
|
||||||
Type: issues_model.PullRequestGitea,
|
Type: issues_model.PullRequestGitea,
|
||||||
}
|
}
|
||||||
err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
prOpts := &pull.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
||||||
|
err = pull.NewPullRequest(git.DefaultContext, prOpts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
||||||
|
|||||||
@ -173,7 +173,8 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
|
|||||||
BaseRepo: baseRepo,
|
BaseRepo: baseRepo,
|
||||||
Type: issues_model.PullRequestGitea,
|
Type: issues_model.PullRequestGitea,
|
||||||
}
|
}
|
||||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
||||||
|
err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
|
||||||
|
|||||||
@ -1388,6 +1388,7 @@ table th[data-sortt-desc] .svg {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.list.flex-items-block > .item,
|
||||||
.flex-items-block > .item,
|
.flex-items-block > .item,
|
||||||
.flex-text-block {
|
.flex-text-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -50,9 +50,20 @@
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */
|
||||||
|
.issue-sidebar-combo .ui.dropdown .menu > .item > img,
|
||||||
|
.issue-sidebar-combo .ui.dropdown .menu > .item > svg {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.issue-content-right .dropdown > .menu {
|
.issue-content-right .dropdown > .menu {
|
||||||
max-width: 270px;
|
max-width: 270px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
@ -62,23 +73,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .issue-content-right .ui.list .dependency {
|
|
||||||
padding: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .issue-content-right .ui.list .title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .issue-content-right #deadlineForm input {
|
|
||||||
width: 12.8rem;
|
|
||||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
|
||||||
border-right: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .issue-content-right .filter.menu {
|
.repository .issue-content-right .filter.menu {
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -118,10 +112,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .select-label .desc {
|
|
||||||
padding-left: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For the secondary pointing menu, respect its own border-bottom */
|
/* For the secondary pointing menu, respect its own border-bottom */
|
||||||
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
||||||
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
||||||
@ -2463,12 +2453,6 @@ tbody.commit-list {
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-file-header {
|
.diff-file-header {
|
||||||
padding: 5px 8px !important;
|
padding: 5px 8px !important;
|
||||||
box-shadow: 0 -1px 0 1px var(--color-body); /* prevent borders being visible behind top corners when sticky and scrolled */
|
box-shadow: 0 -1px 0 1px var(--color-body); /* prevent borders being visible behind top corners when sticky and scrolled */
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.archived-label-hint {
|
.archived-label-hint {
|
||||||
float: right;
|
position: absolute;
|
||||||
margin: -12px;
|
top: 10px;
|
||||||
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,13 @@ export function initGlobalDropdown() {
|
|||||||
const $uiDropdowns = fomanticQuery('.ui.dropdown');
|
const $uiDropdowns = fomanticQuery('.ui.dropdown');
|
||||||
|
|
||||||
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
||||||
$uiDropdowns.filter(':not(.custom)').dropdown();
|
$uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
|
||||||
|
|
||||||
// The "jump" means this dropdown is mainly used for "menu" purpose,
|
// The "jump" means this dropdown is mainly used for "menu" purpose,
|
||||||
// clicking an item will jump to somewhere else or trigger an action/function.
|
// clicking an item will jump to somewhere else or trigger an action/function.
|
||||||
// When a dropdown is used for non-refresh actions with tippy,
|
// When a dropdown is used for non-refresh actions with tippy,
|
||||||
// it must have this "jump" class to hide the tippy when dropdown is closed.
|
// it must have this "jump" class to hide the tippy when dropdown is closed.
|
||||||
$uiDropdowns.filter('.jump').dropdown({
|
$uiDropdowns.filter('.jump').dropdown('setting', {
|
||||||
action: 'hide',
|
action: 'hide',
|
||||||
onShow() {
|
onShow() {
|
||||||
// hide associated tooltip while dropdown is open
|
// hide associated tooltip while dropdown is open
|
||||||
|
|||||||
159
web_src/js/features/repo-issue-sidebar-combolist.ts
Normal file
159
web_src/js/features/repo-issue-sidebar-combolist.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
import {POST} from '../modules/fetch.ts';
|
||||||
|
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
||||||
|
|
||||||
|
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||||
|
function issueSidebarReloadConfirmDraftComment() {
|
||||||
|
const commentTextareas = [
|
||||||
|
document.querySelector<HTMLTextAreaElement>('.edit-content-zone:not(.tw-hidden) textarea'),
|
||||||
|
document.querySelector<HTMLTextAreaElement>('#comment-form textarea'),
|
||||||
|
];
|
||||||
|
for (const textarea of commentTextareas) {
|
||||||
|
// Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
|
||||||
|
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
|
||||||
|
if (textarea && textarea.value.trim().length > 10) {
|
||||||
|
textarea.parentElement.scrollIntoView();
|
||||||
|
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
class IssueSidebarComboList {
|
||||||
|
updateUrl: string;
|
||||||
|
updateAlgo: string;
|
||||||
|
selectionMode: string;
|
||||||
|
elDropdown: HTMLElement;
|
||||||
|
elList: HTMLElement;
|
||||||
|
elComboValue: HTMLInputElement;
|
||||||
|
initialValues: string[];
|
||||||
|
|
||||||
|
constructor(private container: HTMLElement) {
|
||||||
|
this.updateUrl = this.container.getAttribute('data-update-url');
|
||||||
|
this.updateAlgo = container.getAttribute('data-update-algo');
|
||||||
|
this.selectionMode = container.getAttribute('data-selection-mode');
|
||||||
|
if (!['single', 'multiple'].includes(this.selectionMode)) throw new Error(`Invalid data-update-on: ${this.selectionMode}`);
|
||||||
|
if (!['diff', 'all'].includes(this.updateAlgo)) throw new Error(`Invalid data-update-algo: ${this.updateAlgo}`);
|
||||||
|
this.elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
|
||||||
|
this.elList = container.querySelector<HTMLElement>(':scope > .ui.list');
|
||||||
|
this.elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
|
||||||
|
}
|
||||||
|
|
||||||
|
collectCheckedValues() {
|
||||||
|
return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUiList(changedValues) {
|
||||||
|
const elEmptyTip = this.elList.querySelector('.item.empty-list');
|
||||||
|
queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
|
||||||
|
for (const value of changedValues) {
|
||||||
|
const el = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||||
|
if (!el) continue;
|
||||||
|
const listItem = el.cloneNode(true) as HTMLElement;
|
||||||
|
queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
|
||||||
|
this.elList.append(listItem);
|
||||||
|
}
|
||||||
|
const hasItems = Boolean(this.elList.querySelector('.item:not(.empty-list)'));
|
||||||
|
toggleElem(elEmptyTip, !hasItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateToBackend(changedValues) {
|
||||||
|
if (this.updateAlgo === 'diff') {
|
||||||
|
for (const value of this.initialValues) {
|
||||||
|
if (!changedValues.includes(value)) {
|
||||||
|
await POST(this.updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const value of changedValues) {
|
||||||
|
if (!this.initialValues.includes(value)) {
|
||||||
|
await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
|
||||||
|
}
|
||||||
|
issueSidebarReloadConfirmDraftComment();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doUpdate() {
|
||||||
|
const changedValues = this.collectCheckedValues();
|
||||||
|
if (this.initialValues.join(',') === changedValues.join(',')) return;
|
||||||
|
this.updateUiList(changedValues);
|
||||||
|
if (this.updateUrl) await this.updateToBackend(changedValues);
|
||||||
|
this.initialValues = changedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onChange() {
|
||||||
|
if (this.selectionMode === 'single') {
|
||||||
|
await this.doUpdate();
|
||||||
|
fomanticQuery(this.elDropdown).dropdown('hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onItemClick(e) {
|
||||||
|
const elItem = (e.target as HTMLElement).closest('.item');
|
||||||
|
if (!elItem) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
|
||||||
|
|
||||||
|
if (elItem.matches('.clear-selection')) {
|
||||||
|
queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
|
||||||
|
this.elComboValue.value = '';
|
||||||
|
this.onChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = elItem.getAttribute('data-scope');
|
||||||
|
if (scope) {
|
||||||
|
// scoped items could only be checked one at a time
|
||||||
|
const elSelected = this.elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
|
||||||
|
if (elSelected === elItem) {
|
||||||
|
elItem.classList.toggle('checked');
|
||||||
|
} else {
|
||||||
|
queryElems(this.elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
|
||||||
|
elItem.classList.toggle('checked', true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.selectionMode === 'multiple') {
|
||||||
|
elItem.classList.toggle('checked');
|
||||||
|
} else {
|
||||||
|
queryElems(this.elDropdown, `.menu > .item.checked`, (el) => el.classList.remove('checked'));
|
||||||
|
elItem.classList.toggle('checked', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.elComboValue.value = this.collectCheckedValues().join(',');
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onHide() {
|
||||||
|
if (this.selectionMode === 'multiple') this.doUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// init the checked items from initial value
|
||||||
|
if (this.elComboValue.value && this.elComboValue.value !== '0' && !queryElems(this.elDropdown, `.menu > .item.checked`).length) {
|
||||||
|
const values = this.elComboValue.value.split(',');
|
||||||
|
for (const value of values) {
|
||||||
|
const elItem = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||||
|
elItem?.classList.add('checked');
|
||||||
|
}
|
||||||
|
this.updateUiList(values);
|
||||||
|
}
|
||||||
|
this.initialValues = this.collectCheckedValues();
|
||||||
|
|
||||||
|
this.elDropdown.addEventListener('click', (e) => this.onItemClick(e));
|
||||||
|
|
||||||
|
fomanticQuery(this.elDropdown).dropdown('setting', {
|
||||||
|
action: 'nothing', // do not hide the menu if user presses Enter
|
||||||
|
fullTextSearch: 'exact',
|
||||||
|
onHide: () => this.onHide(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initIssueSidebarComboList(container: HTMLElement) {
|
||||||
|
new IssueSidebarComboList(container).init();
|
||||||
|
}
|
||||||
31
web_src/js/features/repo-issue-sidebar.md
Normal file
31
web_src/js/features/repo-issue-sidebar.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
A sidebar combo (dropdown+list) is like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="issue-sidebar-combo" data-selection-mode="..." data-update-url="...">
|
||||||
|
<input class="combo-value" name="..." type="hidden" value="...">
|
||||||
|
<div class="ui dropdown">
|
||||||
|
<div class="menu">
|
||||||
|
<div class="item clear-selection">clear</div>
|
||||||
|
<div class="item" data-value="..." data-scope="...">
|
||||||
|
<span class="item-check-mark">...</span>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui list">
|
||||||
|
<span class="item empty-list">no item</span>
|
||||||
|
<span class="item">...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
When the selected items change, the `combo-value` input will be updated.
|
||||||
|
If there is `data-update-url`, it also calls backend to attach/detach the changed items.
|
||||||
|
|
||||||
|
Also, the changed items will be syncronized to the `ui list` items.
|
||||||
|
|
||||||
|
The items with the same data-scope only allow one selected at a time.
|
||||||
|
|
||||||
|
The dropdown selection could work in 2 modes:
|
||||||
|
* single: only one item could be selected, it updates immediately when the item is selected.
|
||||||
|
* multiple: multiple items could be selected, it defers the update until the dropdown is hidden.
|
||||||
@ -1,29 +1,7 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
import {updateIssuesMeta} from './repo-common.ts';
|
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||||
import {svg} from '../svg.ts';
|
import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
|
||||||
import {htmlEscape} from 'escape-goat';
|
|
||||||
import {toggleElem} from '../utils/dom.ts';
|
|
||||||
|
|
||||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
|
||||||
function reloadConfirmDraftComment() {
|
|
||||||
const commentTextareas = [
|
|
||||||
document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
|
|
||||||
document.querySelector('#comment-form textarea'),
|
|
||||||
];
|
|
||||||
for (const textarea of commentTextareas) {
|
|
||||||
// Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
|
|
||||||
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
|
|
||||||
if (textarea && textarea.value.trim().length > 10) {
|
|
||||||
textarea.parentElement.scrollIntoView();
|
|
||||||
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initBranchSelector() {
|
function initBranchSelector() {
|
||||||
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
|
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
|
||||||
@ -47,218 +25,12 @@ function initBranchSelector() {
|
|||||||
} else {
|
} else {
|
||||||
// for new issue, only update UI&form, do not send request/reload
|
// for new issue, only update UI&form, do not send request/reload
|
||||||
const selectedHiddenSelector = this.getAttribute('data-id-selector');
|
const selectedHiddenSelector = this.getAttribute('data-id-selector');
|
||||||
document.querySelector(selectedHiddenSelector).value = selectedValue;
|
document.querySelector<HTMLInputElement>(selectedHiddenSelector).value = selectedValue;
|
||||||
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// List submits
|
|
||||||
function initListSubmits(selector, outerSelector) {
|
|
||||||
const $list = $(`.ui.${outerSelector}.list`);
|
|
||||||
const $noSelect = $list.find('.no-select');
|
|
||||||
const $listMenu = $(`.${selector} .menu`);
|
|
||||||
let hasUpdateAction = $listMenu.data('action') === 'update';
|
|
||||||
const items = {};
|
|
||||||
|
|
||||||
$(`.${selector}`).dropdown({
|
|
||||||
'action': 'nothing', // do not hide the menu if user presses Enter
|
|
||||||
fullTextSearch: 'exact',
|
|
||||||
async onHide() {
|
|
||||||
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
|
|
||||||
if (hasUpdateAction) {
|
|
||||||
// TODO: Add batch functionality and make this 1 network request.
|
|
||||||
const itemEntries = Object.entries(items);
|
|
||||||
for (const [elementId, item] of itemEntries) {
|
|
||||||
await updateIssuesMeta(
|
|
||||||
item['update-url'],
|
|
||||||
item.action,
|
|
||||||
item['issue-id'],
|
|
||||||
elementId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (itemEntries.length) {
|
|
||||||
reloadConfirmDraftComment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
$listMenu.find('.item:not(.no-select)').on('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.classList.contains('ban-change')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
|
|
||||||
|
|
||||||
const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment
|
|
||||||
const scope = this.getAttribute('data-scope');
|
|
||||||
|
|
||||||
$(this).parent().find('.item').each(function () {
|
|
||||||
if (scope) {
|
|
||||||
// Enable only clicked item for scoped labels
|
|
||||||
if (this.getAttribute('data-scope') !== scope) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this !== clickedItem && !this.classList.contains('checked')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (this !== clickedItem) {
|
|
||||||
// Toggle for other labels
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.classList.contains('checked')) {
|
|
||||||
$(this).removeClass('checked');
|
|
||||||
$(this).find('.octicon-check').addClass('tw-invisible');
|
|
||||||
if (hasUpdateAction) {
|
|
||||||
if (!($(this).data('id') in items)) {
|
|
||||||
items[$(this).data('id')] = {
|
|
||||||
'update-url': $listMenu.data('update-url'),
|
|
||||||
action: 'detach',
|
|
||||||
'issue-id': $listMenu.data('issue-id'),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
delete items[$(this).data('id')];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$(this).addClass('checked');
|
|
||||||
$(this).find('.octicon-check').removeClass('tw-invisible');
|
|
||||||
if (hasUpdateAction) {
|
|
||||||
if (!($(this).data('id') in items)) {
|
|
||||||
items[$(this).data('id')] = {
|
|
||||||
'update-url': $listMenu.data('update-url'),
|
|
||||||
action: 'attach',
|
|
||||||
'issue-id': $listMenu.data('issue-id'),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
delete items[$(this).data('id')];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Which thing should be done for choosing review requests
|
|
||||||
// to make chosen items be shown on time here?
|
|
||||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listIds = [];
|
|
||||||
$(this).parent().find('.item').each(function () {
|
|
||||||
if (this.classList.contains('checked')) {
|
|
||||||
listIds.push($(this).data('id'));
|
|
||||||
$($(this).data('id-selector')).removeClass('tw-hidden');
|
|
||||||
} else {
|
|
||||||
$($(this).data('id-selector')).addClass('tw-hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!listIds.length) {
|
|
||||||
$noSelect.removeClass('tw-hidden');
|
|
||||||
} else {
|
|
||||||
$noSelect.addClass('tw-hidden');
|
|
||||||
}
|
|
||||||
$($(this).parent().data('id')).val(listIds.join(','));
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$listMenu.find('.no-select.item').on('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (hasUpdateAction) {
|
|
||||||
(async () => {
|
|
||||||
await updateIssuesMeta(
|
|
||||||
$listMenu.data('update-url'),
|
|
||||||
'clear',
|
|
||||||
$listMenu.data('issue-id'),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
reloadConfirmDraftComment();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
$(this).parent().find('.item').each(function () {
|
|
||||||
$(this).removeClass('checked');
|
|
||||||
$(this).find('.octicon-check').addClass('tw-invisible');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$list.find('.item').each(function () {
|
|
||||||
$(this).addClass('tw-hidden');
|
|
||||||
});
|
|
||||||
$noSelect.removeClass('tw-hidden');
|
|
||||||
$($(this).parent().data('id')).val('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectItem(select_id, input_id) {
|
|
||||||
const $menu = $(`${select_id} .menu`);
|
|
||||||
const $list = $(`.ui${select_id}.list`);
|
|
||||||
const hasUpdateAction = $menu.data('action') === 'update';
|
|
||||||
|
|
||||||
$menu.find('.item:not(.no-select)').on('click', function () {
|
|
||||||
$(this).parent().find('.item').each(function () {
|
|
||||||
$(this).removeClass('selected active');
|
|
||||||
});
|
|
||||||
|
|
||||||
$(this).addClass('selected active');
|
|
||||||
if (hasUpdateAction) {
|
|
||||||
(async () => {
|
|
||||||
await updateIssuesMeta(
|
|
||||||
$menu.data('update-url'),
|
|
||||||
'',
|
|
||||||
$menu.data('issue-id'),
|
|
||||||
$(this).data('id'),
|
|
||||||
);
|
|
||||||
reloadConfirmDraftComment();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon = '';
|
|
||||||
if (input_id === '#milestone_id') {
|
|
||||||
icon = svg('octicon-milestone', 18, 'tw-mr-2');
|
|
||||||
} else if (input_id === '#project_id') {
|
|
||||||
icon = svg('octicon-project', 18, 'tw-mr-2');
|
|
||||||
} else if (input_id === '#assignee_id') {
|
|
||||||
icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
$list.find('.selected').html(`
|
|
||||||
<a class="item muted sidebar-item-link" href="${htmlEscape(this.getAttribute('data-href'))}">
|
|
||||||
${icon}
|
|
||||||
${htmlEscape(this.textContent)}
|
|
||||||
</a>
|
|
||||||
`);
|
|
||||||
|
|
||||||
$(`.ui${select_id}.list .no-select`).addClass('tw-hidden');
|
|
||||||
$(input_id).val($(this).data('id'));
|
|
||||||
});
|
|
||||||
$menu.find('.no-select.item').on('click', function () {
|
|
||||||
$(this).parent().find('.item:not(.no-select)').each(function () {
|
|
||||||
$(this).removeClass('selected active');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasUpdateAction) {
|
|
||||||
(async () => {
|
|
||||||
await updateIssuesMeta(
|
|
||||||
$menu.data('update-url'),
|
|
||||||
'',
|
|
||||||
$menu.data('issue-id'),
|
|
||||||
$(this).data('id'),
|
|
||||||
);
|
|
||||||
reloadConfirmDraftComment();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
$list.find('.selected').html('');
|
|
||||||
$list.find('.no-select').removeClass('tw-hidden');
|
|
||||||
$(input_id).val('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initRepoIssueDue() {
|
function initRepoIssueDue() {
|
||||||
const form = document.querySelector<HTMLFormElement>('.issue-due-form');
|
const form = document.querySelector<HTMLFormElement>('.issue-due-form');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
@ -276,14 +48,6 @@ export function initRepoIssueSidebar() {
|
|||||||
initBranchSelector();
|
initBranchSelector();
|
||||||
initRepoIssueDue();
|
initRepoIssueDue();
|
||||||
|
|
||||||
// Init labels and assignees
|
// init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
|
||||||
initListSubmits('select-label', 'labels');
|
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
|
||||||
initListSubmits('select-assignees', 'assignees');
|
|
||||||
initListSubmits('select-assignees-modify', 'assignees');
|
|
||||||
initListSubmits('select-reviewers-modify', 'assignees');
|
|
||||||
|
|
||||||
// Milestone, Assignee, Project
|
|
||||||
selectItem('.select-project', '#project_id');
|
|
||||||
selectItem('.select-milestone', '#milestone_id');
|
|
||||||
selectItem('.select-assignee', '#assignee_id');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts';
|
|||||||
import {GET, POST} from '../modules/fetch.ts';
|
import {GET, POST} from '../modules/fetch.ts';
|
||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
|
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
|
||||||
import {updateIssuesMeta} from './repo-common.ts';
|
|
||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
@ -99,6 +98,7 @@ export function initRepoIssueSidebarList() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: it is wrong place to init ".ui.dropdown.label-filter"
|
||||||
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
|
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
|
||||||
if (e.altKey && e.key === 'Enter') {
|
if (e.altKey && e.key === 'Enter') {
|
||||||
const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
|
const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
|
||||||
@ -107,7 +107,6 @@ export function initRepoIssueSidebarList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoIssueCommentDelete() {
|
export function initRepoIssueCommentDelete() {
|
||||||
@ -326,17 +325,6 @@ export function initRepoIssueWipTitle() {
|
|||||||
export function initRepoIssueComments() {
|
export function initRepoIssueComments() {
|
||||||
if (!$('.repository.view.issue .timeline').length) return;
|
if (!$('.repository.view.issue .timeline').length) return;
|
||||||
|
|
||||||
$('.re-request-review').on('click', async function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const url = this.getAttribute('data-update-url');
|
|
||||||
const issueId = this.getAttribute('data-issue-id');
|
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
const isChecked = this.classList.contains('checked');
|
|
||||||
|
|
||||||
await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const urlTarget = document.querySelector(':target');
|
const urlTarget = document.querySelector(':target');
|
||||||
if (!urlTarget) return;
|
if (!urlTarget) return;
|
||||||
@ -664,19 +652,6 @@ function initIssueTemplateCommentEditors($commentForm) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function used to show and hide archived label on issue/pr
|
|
||||||
// page in the sidebar where we select the labels
|
|
||||||
// If we have any archived label tagged to issue and pr. We will show that
|
|
||||||
// archived label with checked classed otherwise we will hide it
|
|
||||||
// with the help of this function.
|
|
||||||
// This function runs globally.
|
|
||||||
export function initArchivedLabelHandler() {
|
|
||||||
if (!document.querySelector('.archived-label-hint')) return;
|
|
||||||
for (const label of document.querySelectorAll('[data-is-archived]')) {
|
|
||||||
toggleElem(label, label.classList.contains('checked'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initRepoCommentFormAndSidebar() {
|
export function initRepoCommentFormAndSidebar() {
|
||||||
const $commentForm = $('.comment.form');
|
const $commentForm = $('.comment.form');
|
||||||
if (!$commentForm.length) return;
|
if (!$commentForm.length) return;
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
initRepoIssueWipTitle,
|
initRepoIssueWipTitle,
|
||||||
initRepoPullRequestMergeInstruction,
|
initRepoPullRequestMergeInstruction,
|
||||||
initRepoPullRequestAllowMaintainerEdit,
|
initRepoPullRequestAllowMaintainerEdit,
|
||||||
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
|
initRepoPullRequestReview, initRepoIssueSidebarList,
|
||||||
} from './features/repo-issue.ts';
|
} from './features/repo-issue.ts';
|
||||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||||
import {initRepoTopicBar} from './features/repo-home.ts';
|
import {initRepoTopicBar} from './features/repo-home.ts';
|
||||||
@ -182,7 +182,6 @@ onDomReady(() => {
|
|||||||
initRepoIssueContentHistory,
|
initRepoIssueContentHistory,
|
||||||
initRepoIssueList,
|
initRepoIssueList,
|
||||||
initRepoIssueSidebarList,
|
initRepoIssueSidebarList,
|
||||||
initArchivedLabelHandler,
|
|
||||||
initRepoIssueReferenceRepositorySearch,
|
initRepoIssueReferenceRepositorySearch,
|
||||||
initRepoIssueTimeTracking,
|
initRepoIssueTimeTracking,
|
||||||
initRepoIssueWipTitle,
|
initRepoIssueWipTitle,
|
||||||
|
|||||||
@ -57,10 +57,21 @@ export async function renderMermaid() {
|
|||||||
btn.setAttribute('data-clipboard-text', source);
|
btn.setAttribute('data-clipboard-text', source);
|
||||||
mermaidBlock.append(btn);
|
mermaidBlock.append(btn);
|
||||||
|
|
||||||
|
const updateIframeHeight = () => {
|
||||||
|
iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// update height when element's visibility state changes, for example when the diagram is inside
|
||||||
|
// a <details> + <summary> block and the <details> block becomes visible upon user interaction, it
|
||||||
|
// would initially set a incorrect height and the correct height is set during this callback.
|
||||||
|
(new IntersectionObserver(() => {
|
||||||
|
updateIframeHeight();
|
||||||
|
}, {root: document.documentElement})).observe(iframe);
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
pre.replaceWith(mermaidBlock);
|
pre.replaceWith(mermaidBlock);
|
||||||
mermaidBlock.classList.remove('tw-hidden');
|
mermaidBlock.classList.remove('tw-hidden');
|
||||||
iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
|
updateIframeHeight();
|
||||||
setTimeout(() => { // avoid flash of iframe background
|
setTimeout(() => { // avoid flash of iframe background
|
||||||
mermaidBlock.classList.remove('is-loading');
|
mermaidBlock.classList.remove('is-loading');
|
||||||
iframe.classList.remove('tw-invisible');
|
iframe.classList.remove('tw-invisible');
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest';
|
|||||||
import type $ from 'jquery';
|
import type $ from 'jquery';
|
||||||
|
|
||||||
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
||||||
type ElementsCallback = (el: Element) => Promisable<any>;
|
type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
|
||||||
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
||||||
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
|
|||||||
return res[0];
|
return res[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> {
|
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
for (const el of elems) {
|
for (const el of elems) {
|
||||||
fn(el);
|
fn(el);
|
||||||
@ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?:
|
|||||||
return elems;
|
return elems;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
|
export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
const elems = Array.from(el.parentNode.children) as T[];
|
const elems = Array.from(el.parentNode.children) as T[];
|
||||||
return applyElemsCallback<T>(elems.filter((child: Element) => {
|
return applyElemsCallback<T>(elems.filter((child: Element) => {
|
||||||
return child !== el && child.matches(selector);
|
return child !== el && child.matches(selector);
|
||||||
@ -75,13 +75,13 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
|
|||||||
}
|
}
|
||||||
|
|
||||||
// it works like jQuery.children: only the direct children are selected
|
// it works like jQuery.children: only the direct children are selected
|
||||||
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
|
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// it works like parent.querySelectorAll: all descendants are selected
|
// it works like parent.querySelectorAll: all descendants are selected
|
||||||
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
|
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
|
||||||
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> {
|
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
|
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user