Merge branch 'main' into feat-32257-add-comments-unchanged-lines-and-show

This commit is contained in:
Rajesh Jonnalagadda 2024-11-11 07:20:45 +05:30 committed by GitHub
commit 914bed86ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 1283 additions and 1194 deletions

View File

@ -1007,6 +1007,14 @@ LEVEL = Info
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
;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 = true
;;

View File

@ -141,8 +141,9 @@ func (org *Organization) LoadTeams(ctx context.Context) ([]*Team, error) {
}
// 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{
Doer: doer,
OrgID: org.ID,
})
}
@ -195,16 +196,22 @@ func (org *Organization) CanCreateRepo() bool {
// FindOrgMembersOpts represensts find org members conditions
type FindOrgMembersOpts struct {
db.ListOptions
Doer *user_model.User
IsDoerMember bool
OrgID int64
PublicOnly bool
}
func (opts FindOrgMembersOpts) PublicOnly() bool {
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
}
// CountOrgMembers counts the organization's members
func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, error) {
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
if opts.PublicOnly {
if opts.PublicOnly() {
sess.And("is_public = ?", true)
}
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.
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
if opts.PublicOnly {
if opts.PublicOnly() {
sess.And("is_public = ?", true)
}
if opts.ListOptions.PageSize > 0 {
sess = db.SetSessionPagination(sess, opts)

View File

@ -4,6 +4,7 @@
package organization_test
import (
"sort"
"testing"
"code.gitea.io/gitea/models/db"
@ -103,7 +104,7 @@ func TestUser_GetTeams(t *testing.T) {
func TestUser_GetMembers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
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)
if assert.Len(t, members, 3) {
assert.Equal(t, int64(2), members[0].ID)
@ -210,37 +211,42 @@ func TestFindOrgs(t *testing.T) {
func TestGetOrgUsersByOrgID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
ListOptions: db.ListOptions{},
opts := &organization.FindOrgMembersOpts{
Doer: &user_model.User{IsAdmin: true},
OrgID: 3,
PublicOnly: false,
})
}
assert.False(t, opts.PublicOnly())
orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
assert.NoError(t, err)
if assert.Len(t, orgUsers, 3) {
assert.Equal(t, organization.OrgUser{
ID: orgUsers[0].ID,
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,
}, *orgUsers[0])
assert.Equal(t, organization.OrgUser{
ID: orgUsers[1].ID,
}, {
ID: 2,
OrgID: 3,
UID: 4,
IsPublic: false,
}, *orgUsers[1])
assert.Equal(t, organization.OrgUser{
ID: orgUsers[2].ID,
}, {
ID: 9,
OrgID: 3,
UID: 28,
IsPublic: true,
}, *orgUsers[2])
}
}}, 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{
ListOptions: db.ListOptions{},
OrgID: unittest.NonexistentID,
PublicOnly: false,
})
assert.NoError(t, err)
assert.Len(t, orgUsers, 0)

View File

@ -94,7 +94,7 @@ func TestUserListIsPublicMember(t *testing.T) {
func testUserListIsPublicMember(t *testing.T, orgID int64, expected map[int64]bool) {
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
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.Equal(t, expected, membersIsPublic)
}
@ -121,7 +121,7 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
func testUserListIsUserOrgOwner(t *testing.T, orgID int64, expected map[int64]bool) {
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
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.Equal(t, expected, organization.IsUserOrgOwner(db.DefaultContext, members, orgID))
}

View File

@ -80,6 +80,27 @@ var (
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 = []Type{
TypeExternalWiki,
@ -147,6 +168,7 @@ func LoadUnitConfig() error {
if len(DefaultRepoUnits) == 0 {
return errors.New("no default repository units found")
}
// default fork repo units
setDefaultForkRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultForkRepoUnits...)
if len(invalidKeys) > 0 {
log.Warn("Invalid keys in default fork repo units: %s", strings.Join(invalidKeys, ", "))
@ -155,6 +177,24 @@ func LoadUnitConfig() error {
if len(DefaultForkRepoUnits) == 0 {
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
}

View File

@ -147,6 +147,9 @@ func StringsToInt64s(strs []string) ([]int64, error) {
}
ints := make([]int64, 0, len(strs))
for _, s := range strs {
if s == "" {
continue
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, err

View File

@ -152,6 +152,7 @@ func TestStringsToInt64s(t *testing.T) {
}
testSuccess(nil, nil)
testSuccess([]string{}, []int64{})
testSuccess([]string{""}, []int64{})
testSuccess([]string{"-1234"}, []int64{-1234})
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})

View File

@ -31,8 +31,8 @@ func (s Set[T]) AddMultiple(values ...T) {
}
}
// Contains determines whether a set contains the specified elements.
// Returns true if the set contains the specified element; otherwise, false.
// Contains determines whether a set contains all these elements.
// Returns true if the set contains all these elements; otherwise, false.
func (s Set[T]) Contains(values ...T) bool {
ret := true
for _, value := range values {

View File

@ -18,7 +18,9 @@ func TestSet(t *testing.T) {
assert.True(t, s.Contains("key1"))
assert.True(t, s.Contains("key2"))
assert.True(t, s.Contains("key1", "key2"))
assert.False(t, s.Contains("key3"))
assert.False(t, s.Contains("key1", "key3"))
assert.True(t, s.Remove("key2"))
assert.False(t, s.Contains("key2"))

View File

@ -43,6 +43,8 @@ var (
DisabledRepoUnits []string
DefaultRepoUnits []string
DefaultForkRepoUnits []string
DefaultMirrorRepoUnits []string
DefaultTemplateRepoUnits []string
PrefixArchiveFiles bool
DisableMigrations bool
DisableStars bool `ini:"DISABLE_STARS"`
@ -161,6 +163,8 @@ var (
DisabledRepoUnits: []string{},
DefaultRepoUnits: []string{},
DefaultForkRepoUnits: []string{},
DefaultMirrorRepoUnits: []string{},
DefaultTemplateRepoUnits: []string{},
PrefixArchiveFiles: true,
DisableMigrations: false,
DisableStars: false,

View File

@ -87,6 +87,8 @@ type CreatePullRequestOption struct {
Labels []int64 `json:"labels"`
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
Reviewers []string `json:"reviewers"`
TeamReviewers []string `json:"team_reviewers"`
}
// EditPullRequestOption options when modify pull request

View File

@ -31,6 +31,7 @@ func NewFuncMap() template.FuncMap {
"ctx": func() any { return nil }, // template context function
"DumpVar": dumpVar,
"NIL": func() any { return nil },
// -----------------------------------------------------------------
// html/template related functions

View File

@ -1462,7 +1462,7 @@ issues.new.closed_milestone = Closed Milestones
issues.new.assignees = Assignees
issues.new.clear_assignees = Clear 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.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.

View File

@ -18,10 +18,11 @@ import (
)
// listMembers list an organization's members
func listMembers(ctx *context.APIContext, publicOnly bool) {
func listMembers(ctx *context.APIContext, isMember bool) {
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
IsDoerMember: isMember,
OrgID: ctx.Org.Organization.ID,
PublicOnly: publicOnly,
ListOptions: utils.GetListOptions(ctx),
}
@ -73,16 +74,19 @@ func ListMembers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
publicOnly := true
var (
isMember bool
err error
)
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 {
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
return
}
publicOnly = !isMember && !ctx.Doer.IsAdmin
}
listMembers(ctx, publicOnly)
listMembers(ctx, isMember)
}
// ListPublicMembers list an organization's public members
@ -112,7 +116,7 @@ func ListPublicMembers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listMembers(ctx, true)
listMembers(ctx, false)
}
// IsMember check if a user is a member of an organization

View File

@ -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) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
} else if errors.Is(err, user_model.ErrBlockedUser) {

View File

@ -656,6 +656,47 @@ func DeleteReviewRequests(ctx *context.APIContext) {
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) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
if err != nil {
@ -672,43 +713,16 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
return
}
reviewers := make([]*user_model.User, 0, len(opts.Reviewers))
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
}
for _, r := range opts.Reviewers {
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))
reviewers, teamReviewers := parseReviewersByNames(ctx, opts.Reviewers, opts.TeamReviewers)
if ctx.Written() {
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
if isAdd {
@ -716,12 +730,16 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
}
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 issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.Error(http.StatusForbidden, "", err)
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
return
}
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
return
}
@ -736,35 +754,17 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
}
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 {
comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
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)
return
}

View File

@ -95,10 +95,12 @@ func home(ctx *context.Context, viewRepositories bool) {
}
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
OrgID: org.ID,
PublicOnly: ctx.Org.PublicMemberOnly,
IsDoerMember: ctx.Org.IsMember,
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
}
members, _, err := organization.FindOrgMembers(ctx, opts)
if err != nil {
ctx.ServerError("FindOrgMembers", err)

View File

@ -34,8 +34,8 @@ func Members(ctx *context.Context) {
}
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
OrgID: org.ID,
PublicOnly: true,
}
if ctx.Doer != nil {
@ -44,9 +44,9 @@ func Members(ctx *context.Context) {
ctx.Error(http.StatusInternalServerError, "IsOrgMember")
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)
if err != nil {

View File

@ -790,10 +790,14 @@ func CompareDiff(ctx *context.Context) {
if !nothingToCompare {
// Setup information for new form.
RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, true)
if ctx.Written() {
return
}
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
}
}
}
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["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 a template content is set, prepend the "content". In this case that's only

View File

@ -431,7 +431,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return 0
}
retrieveProjects(ctx, repo)
retrieveProjectsForIssueList(ctx, repo)
if ctx.Written() {
return
}
@ -556,37 +556,147 @@ func renderMilestones(ctx *context.Context) {
ctx.Data["ClosedMilestones"] = closedMilestones
}
// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
type issueSidebarMilestoneData struct {
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
ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
if d.Issue != nil {
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),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
return
}
ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
d.MilestonesData.ClosedMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: d.Repository.ID,
IsClosed: optional.Some(true),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
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 {
ctx.ServerError("GetRepoAssignees", err)
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)
}
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
// is an individual or an organization
repoOwnerType := project_model.TypeIndividual
@ -609,7 +719,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
return nil, nil
}
closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptionsAll,
@ -619,7 +729,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
return nil, nil
}
}
@ -632,7 +742,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
return nil, nil
}
openProjects = append(openProjects, openProjects2...)
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
@ -643,13 +753,11 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
return nil, nil
}
closedProjects = append(closedProjects, closedProjects2...)
}
ctx.Data["OpenProjects"] = openProjects
ctx.Data["ClosedProjects"] = closedProjects
return openProjects, closedProjects
}
// repoReviewerSelection items to bee shown
@ -658,31 +766,62 @@ type repoReviewerSelection struct {
Team *organization.Team
User *user_model.User
Review *issues_model.Review
CanBeDismissed bool
CanChange bool
Checked bool
Requested bool
ItemID int64
}
// RetrieveRepoReviewers find all reviewers of a repository
func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
ctx.Data["CanChooseReviewer"] = canChooseReviewer
type issueSidebarReviewersData struct {
CanChooseReviewer bool
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.
func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
data := d.ReviewersData
repo := d.Repository
if ctx.Doer != nil && ctx.IsSigned {
if d.Issue == nil {
data.CanChooseReviewer = true
} else {
data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue)
}
}
var posterID int64
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
}
ctx.Data["OriginalReviews"] = originalAuthorReviews
data.OriginalReviews = originalAuthorReviews
reviews, err := issues_model.GetReviewsByIssueID(ctx, issue.ID)
reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
if err != nil {
ctx.ServerError("GetReviewersByIssueID", err)
return
}
if len(reviews) == 0 && !canChooseReviewer {
if len(reviews) == 0 && !data.CanChooseReviewer {
return
}
}
var (
pullReviews []*repoReviewerSelection
@ -692,12 +831,8 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
reviewers []*user_model.User
)
if canChooseReviewer {
posterID := issue.PosterID
if issue.OriginalAuthorID > 0 {
posterID = 0
}
if data.CanChooseReviewer {
var err error
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
if err != nil {
ctx.ServerError("GetReviewers", err)
@ -723,7 +858,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
for _, review := range reviews {
tmp := &repoReviewerSelection{
Checked: review.Type == issues_model.ReviewTypeRequest,
Requested: review.Type == issues_model.ReviewTypeRequest,
Review: review,
ItemID: review.ReviewerID,
}
@ -732,7 +867,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
tmp.ItemID = -review.ReviewerTeamID
}
if canChooseReviewer {
if data.CanChooseReviewer {
// Users who can choose reviewers can also remove review requests
tmp.CanChange = true
} 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)
if canChooseReviewer {
if data.CanChooseReviewer {
if tmp.IsTeam {
teamReviewersResult = append(teamReviewersResult, tmp)
} else {
@ -756,7 +891,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
for _, item := range pullReviews {
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) {
continue
}
@ -765,7 +900,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
}
item.User = item.Review.Reviewer
} 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) {
continue
}
@ -776,13 +911,14 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
} else {
continue
}
item.CanBeDismissed = ctx.Repo.Permission.IsAdmin() && !isClosed &&
(item.Review.Type == issues_model.ReviewTypeApprove || item.Review.Type == issues_model.ReviewTypeReject)
currentPullReviewers = append(currentPullReviewers, item)
}
ctx.Data["PullReviewers"] = currentPullReviewers
data.CurrentPullReviewers = currentPullReviewers
}
if canChooseReviewer && reviewersResult != nil {
if data.CanChooseReviewer && reviewersResult != nil {
preadded := len(reviewersResult)
for _, reviewer := range reviewers {
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)
for _, team := range teamReviewers {
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
func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
return nil
type issueSidebarLabelsData struct {
AllLabels []*issues_model.Label
RepoLabels []*issues_model.Label
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{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return nil
return
}
ctx.Data["Labels"] = labels
labelsData.RepoLabels = labels
if repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
return nil
return
}
ctx.Data["OrgLabels"] = orgLabels
labels = append(labels, orgLabels...)
labelsData.OrgLabels = orgLabels
}
RetrieveRepoMilestonesAndAssignees(ctx, repo)
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
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
}
// 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)
if err != nil {
return false, nil
@ -920,43 +1083,21 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
ctx.Data["Fields"] = template.Fields
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 {
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))
metaData.LabelsData.SetSelectedLabelNames(template.Labels)
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 {
selectedAssigneeIDs = append(selectedAssigneeIDs, userID)
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>
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["RefEndName"] = git.RefName(template.Ref).ShortName()
return true, templateErrs
@ -983,36 +1124,19 @@ func NewIssue(ctx *context.Context) {
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
milestoneID := ctx.FormInt64("milestone")
if milestoneID > 0 {
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
if err != nil {
log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
} else {
ctx.Data["milestone_id"] = milestoneID
ctx.Data["Milestone"] = milestone
}
}
projectID := ctx.FormInt64("project")
if projectID > 0 && isProjectsEnabled {
project, err := project_model.GetProjectByID(ctx, projectID)
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
pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, false)
if ctx.Written() {
return
}
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project")
if pageMetaData.ProjectsData.SelectedProjectID > 0 {
if len(ctx.Req.URL.Query().Get("project")) > 0 {
ctx.Data["redirect_after_creation"] = "project"
}
}
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
@ -1021,7 +1145,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
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 {
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)
}
// ValidateRepoMetas check and returns repository's meta information
func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
var (
repo = ctx.Repo.Repository
err error
)
func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(ItemType) KeyType) container.Set[KeyType] {
s := make(container.Set[KeyType])
for _, item := range slice {
s.Add(keyFunc(item))
}
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() {
return nil, nil, 0, 0
return ret
}
var labelIDs []int64
hasSelected := false
// Check labels.
if len(form.LabelIDs) > 0 {
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
if err != nil {
return nil, nil, 0, 0
}
labelIDMark := make(container.Set[int64])
labelIDMark.AddMultiple(labelIDs...)
for i := range labels {
if labelIDMark.Contains(labels[i].ID) {
labels[i].IsChecked = true
hasSelected = true
}
}
}
ctx.Data["Labels"] = labels
ctx.Data["HasSelectedLabel"] = hasSelected
ctx.Data["label_ids"] = form.LabelIDs
// 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 {
inputLabelIDs, _ := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
candidateLabels := toSet(pageMetaData.LabelsData.AllLabels, func(label *issues_model.Label) int64 { return label.ID })
if len(inputLabelIDs) > 0 && !candidateLabels.Contains(inputLabelIDs...) {
ctx.NotFound("", nil)
return nil, nil, 0, 0
return ret
}
pageMetaData.LabelsData.SetSelectedLabelIDs(inputLabelIDs)
ctx.Data["Project"] = p
ctx.Data["project_id"] = form.ProjectID
allMilestones := append(slices.Clone(pageMetaData.MilestonesData.OpenMilestones), pageMetaData.MilestonesData.ClosedMilestones...)
candidateMilestones := toSet(allMilestones, func(milestone *issues_model.Milestone) int64 { return milestone.ID })
if form.MilestoneID > 0 && !candidateMilestones.Contains(form.MilestoneID) {
ctx.NotFound("", nil)
return ret
}
pageMetaData.MilestonesData.SelectedMilestoneID = form.MilestoneID
// 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
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
// 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
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
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
if err != nil {
ctx.ServerError("CanBeAssigned", err)
return nil, nil, 0, 0
// 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
}
if !valid {
ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
return nil, nil, 0, 0
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)
}
}
}
// Keep the old assignee id thingy for compatibility reasons
if form.AssigneeID > 0 {
assigneeIDs = append(assigneeIDs, form.AssigneeID)
}
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, form.ProjectID
ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers
return ret
}
// NewIssuePost response for creating new issue
@ -1234,11 +1346,13 @@ func NewIssuePost(ctx *context.Context) {
attachments []string
)
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false)
if ctx.Written() {
return
}
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
if projectID > 0 {
if !ctx.Repo.CanRead(unit.TypeProjects) {
// User must also be able to see the project.
@ -1507,60 +1621,11 @@ func ViewIssue(ctx *context.Context) {
}
}
// Metas.
// Check labels.
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
}
ctx.Data["Labels"] = 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)
pageMetaData := retrieveRepoIssueMetaData(ctx, repo, issue, issue.IsPull)
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
}
}
pageMetaData.LabelsData.SetSelectedLabels(issue.Labels)
if ctx.IsSigned {
// Update issue-user.
@ -2479,7 +2544,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
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 issues_model.IsErrNotValidReviewRequest(err) {
log.Warn(
@ -2490,12 +2555,6 @@ func UpdatePullReviewRequest(ctx *context.Context) {
ctx.Status(http.StatusForbidden)
return
}
ctx.ServerError("IsValidTeamReviewRequest", err)
return
}
_, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
if err != nil {
ctx.ServerError("TeamReviewRequest", err)
return
}
@ -2517,7 +2576,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
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 issues_model.IsErrNotValidReviewRequest(err) {
log.Warn(
@ -2528,12 +2587,6 @@ func UpdatePullReviewRequest(ctx *context.Context) {
ctx.Status(http.StatusForbidden)
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) {
ctx.Status(http.StatusForbidden)
return

View File

@ -53,11 +53,11 @@ func InitializeLabels(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
// RetrieveLabels find all the labels of a repository and organization
func RetrieveLabels(ctx *context.Context) {
// RetrieveLabelsForList find all the labels of a repository and organization, it is only used by "/labels" page to list all labels
func RetrieveLabelsForList(ctx *context.Context) {
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
ctx.ServerError("RetrieveLabelsForList.GetLabels", err)
return
}

View File

@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, testCase.RepoID)
ctx.Req.Form.Set("sort", testCase.Sort)
RetrieveLabels(ctx)
RetrieveLabelsForList(ctx)
assert.False(t, ctx.Written())
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
assert.True(t, ok)

View File

@ -1269,11 +1269,13 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true)
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
if ctx.Written() {
return
}
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
if setting.Attachment.Enabled {
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
// instead of 500.
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
prOpts := &pull_service.NewPullRequestOptions{
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 {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())

View File

@ -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("/pulls/posters", repo.PullPosters)
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("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
m.Group("/{type:issues|pulls}", func() {

View File

@ -137,8 +137,12 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
Type: issues_model.PullRequestGitea,
Flow: issues_model.PullRequestFlowAGit,
}
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
prOpts := &pull_service.NewPullRequestOptions{
Repo: repo,
Issue: prIssue,
PullRequest: pr,
}
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
return nil, err
}

View File

@ -26,7 +26,6 @@ type Organization struct {
Organization *organization.Organization
OrgLink string
CanCreateOrgRepo bool
PublicMemberOnly bool // Only display public members
Team *organization.Team
Teams []*organization.Team
@ -176,10 +175,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
ctx.Data["OrgLink"] = ctx.Org.OrgLink
// Member
ctx.Org.PublicMemberOnly = ctx.Doer == nil || !ctx.Org.IsMember && !ctx.Doer.IsAdmin
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
OrgID: org.ID,
PublicOnly: ctx.Org.PublicMemberOnly,
IsDoerMember: ctx.Org.IsMember,
}
ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts)
if err != nil {

View 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,
})
}

View File

@ -447,10 +447,10 @@ type CreateIssueForm struct {
Title string `binding:"Required;MaxSize(255)"`
LabelIDs string `form:"label_ids"`
AssigneeIDs string `form:"assignee_ids"`
ReviewerIDs string `form:"reviewer_ids"`
Ref string `form:"ref"`
MilestoneID int64
ProjectID int64
AssigneeID int64
Content string
Files []string
AllowMaintainerEdit bool

View File

@ -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.
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 {
comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer)
} else {
@ -79,8 +84,8 @@ func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewe
return comment, err
}
// 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 {
// 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 {
if reviewer.IsOrganization() {
return issues_model.ErrNotValidReviewRequest{
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) {
return err
}
@ -137,7 +142,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
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
}
@ -152,7 +157,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
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
}
@ -163,8 +168,8 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
}
}
// 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 {
// 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 {
if doer.IsOrganization() {
return issues_model.ErrNotValidReviewRequest{
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.
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 {
comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer)
} 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
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
if doer.ID == issue.PosterID {
return true

View File

@ -382,7 +382,7 @@ func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error {
if _, err := msg.WriteTo(&buf); err != nil {
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
}

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
@ -41,8 +42,20 @@ func getPullWorkingLockKey(prID int64) string {
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.
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 {
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])
}
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
}

View File

@ -381,8 +381,13 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
// insert units for repo
defaultUnits := unit.DefaultRepoUnits
if isFork {
switch {
case isFork:
defaultUnits = unit.DefaultForkRepoUnits
case repo.IsMirror:
defaultUnits = unit.DefaultMirrorRepoUnits
case repo.IsTemplate:
defaultUnits = unit.DefaultTemplateRepoUnits
}
units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
for _, tp := range defaultUnits {

View File

@ -44,4 +44,5 @@
</div>
</div>
</div>
<div class="divider"></div>
{{end}}

View File

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

View File

@ -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>&nbsp;&nbsp;{{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>&nbsp;&nbsp;{{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>

View File

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

View File

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

View File

@ -18,15 +18,15 @@
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
{{range .Fields}}
{{if eq .Type "input"}}
{{template "repo/issue/fields/input" "item" .}}
{{template "repo/issue/fields/input" dict "item" .}}
{{else if eq .Type "markdown"}}
{{template "repo/issue/fields/markdown" "item" .}}
{{template "repo/issue/fields/markdown" dict "item" .}}
{{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"}}
{{template "repo/issue/fields/dropdown" "item" .}}
{{template "repo/issue/fields/dropdown" dict "item" .}}
{{else if eq .Type "checkboxes"}}
{{template "repo/issue/fields/checkboxes" "item" .}}
{{template "repo/issue/fields/checkboxes" dict "item" .}}
{{end}}
{{end}}
{{else}}
@ -47,143 +47,25 @@
</div>
<div class="issue-content-right ui segment">
{{template "repo/issue/branch_selector_field" .}}
<input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}">
{{template "repo/issue/labels/labels_selector_field" .}}
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
{{template "repo/issue/branch_selector_field" $}}
{{if .PageIsComparePull}}
{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
<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>
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
{{if .IsProjectsEnabled}}
<div class="divider"></div>
{{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}
{{end}}
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
<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}}
<div class="divider"></div>
<input id="assignee_ids" name="assignee_ids" type="hidden" value="{{.assignee_ids}}">
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating jump select-assignees dropdown">
<span class="text flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
{{if .HasIssuesOrPullsWritePermission}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{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}}
<div class="divider"></div>
<div class="inline field">
<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>
<input name="allow_maintainer_edit" type="checkbox" {{if .AllowMaintainerEdit}}checked{{end}}>
</div>
</div>
{{end}}
</div>
<input type="hidden" name="redirect_after_creation" value="{{.redirect_after_creation}}">

View File

@ -1,7 +1,6 @@
{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
data-url="{{.Issue.Link}}"
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
@ -10,6 +9,5 @@
<label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
<input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}}>
</div>
</div>
{{end}}
{{end}}

View File

@ -1,46 +1,35 @@
{{$pageMeta := .}}
{{$data := .AssigneesData}}
{{$issueAssignees := NIL}}{{if $pageMeta.Issue}}{{$issueAssignees = $pageMeta.Issue.Assignees}}{{end}}
<div class="divider"></div>
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-assignees-modify dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
<div class="issue-sidebar-combo" data-selection-mode="multiple" data-update-algo="diff"
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/assignee?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="assignee_ids" type="hidden" value="{{$data.SelectedAssigneeIDs}}">
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</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">
<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}}
{{$AssigneeID := .ID}}
<a class="item{{range $.Issue.Assignees}}{{if eq .ID $AssigneeID}} checked{{end}}{{end}}" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
{{$checked := false}}
{{range $.Issue.Assignees}}
{{if eq .ID $AssigneeID}}
{{$checked = true}}
<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}}
{{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>
</div>
</div>
<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>
{{range $issueAssignees}}
<a class="item muted" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
</a>
{{end}}
</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>

View File

@ -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}}">
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
</span>
<div class="ui relaxed divided list">
<div class="ui divided list">
{{range .BlockingDependencies}}
<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">
<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}}
</a>
<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}}">
<strong>{{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
</span>
<div class="ui relaxed divided list">
<div class="ui divided list">
{{range .BlockedByDependencies}}
<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">
<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}}
</a>
<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="gt-ellipsis">
<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}}
</span>
</div>

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

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

View File

@ -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="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-milestone dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
<div class="issue-sidebar-combo" data-selection-mode="single" data-update-algo="all"
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/milestone?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="milestone_id" type="hidden" value="{{$data.SelectedMilestoneID}}">
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}} ">
<a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
{{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>
<div class="ui select-milestone list">
<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
<div class="selected">
{{if .Issue.Milestone}}
<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
{{svg "octicon-milestone" 18 "tw-mr-2"}}
{{.Issue.Milestone.Name}}
<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 class="ui list">
<span class="item empty-list {{if $issueMilestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
{{if $issueMilestone}}
<a class="item muted" href="{{$pageMeta.RepoLink}}/milestone/{{$issueMilestone.ID}}">
{{svg "octicon-milestone" 18}} {{$issueMilestone.Name}}
</a>
{{end}}
</div>

View File

@ -4,7 +4,7 @@
<div class="ui list tw-flex tw-flex-wrap">
{{range .Participants}}
<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>
{{end}}
</div>

View File

@ -1,53 +1,49 @@
{{if .IsProjectsEnabled}}
<div class="divider"></div>
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-project dropdown">
<a class="text muted flex-text-block">
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
{{$pageMeta := .}}
{{$data := .ProjectsData}}
{{$issueProject := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Project}}{{$issueProject = $pageMeta.Issue.Project}}{{end}}
<div class="divider"></div>
<div class="issue-sidebar-combo" data-selection-mode="single" data-update-algo="all"
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="project_id" type="hidden" value="{{$data.SelectedProjectID}}">
<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>
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects">
{{if or .OpenProjects .ClosedProjects}}
<div class="menu">
{{if or $data.OpenProjects $data.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 .OpenProjects}}
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
{{if $data.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}}
<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_projects"}}</div>
{{range $data.OpenProjects}}
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
{{svg .IconName 18}} {{.Title}}
</a>
{{end}}
{{end}}
{{if .ClosedProjects}}
{{if $data.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}}
<div class="header">{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}</div>
{{range $data.ClosedProjects}}
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
{{svg .IconName 18}} {{.Title}}
</a>
{{end}}
{{end}}
</div>
</div>
<div class="ui select-project list">
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
<div class="selected">
{{if .Issue.Project}}
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
<div class="ui list">
<span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
{{if $issueProject}}
<a class="item muted" href="{{$issueProject.Link ctx}}">
{{svg $issueProject.IconName 18}} {{$issueProject.Title}}
</a>
{{end}}
</div>
</div>
{{end}}
</div>

View File

@ -1,66 +1,110 @@
<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
<a class="text tw-flex tw-items-center muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
{{svg "octicon-gear" 16 "tw-ml-1"}}
{{end}}
{{$pageMeta := .}}
{{$data := .ReviewersData}}
{{$repoOwnerName := $pageMeta.Repository.OwnerName}}
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
<div class="issue-sidebar-combo" data-selection-mode="multiple" data-update-algo="diff"
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/request_review?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
<div class="ui dropdown {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
<a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review">
{{if .Reviewers}}
<div class="menu flex-items-menu">
{{if $hasCandidates}}
<div class="ui icon search input">
<i class="icon">{{svg "octicon-search" 16}}</i>
<i class="icon">{{svg "octicon-search"}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_reviewers"}}">
</div>
{{end}}
{{if .Reviewers}}
{{range .Reviewers}}
{{range $data.Reviewers}}
{{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}}>
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
<span class="text">
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}}
</span>
<a class="item muted {{if .Requested}}checked{{end}}" href="{{.User.HomeLink}}" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
{{if not .CanChange}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
<span class="item-check-mark">{{svg "octicon-check"}}</span>
{{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
</a>
{{end}}
{{end}}
{{end}}
{{if .TeamReviewers}}
{{if .Reviewers}}
<div class="divider"></div>
{{end}}
{{range .TeamReviewers}}
{{if $data.TeamReviewers}}
{{if $data.Reviewers}}<div class="divider"></div>{{end}}
{{range $data.TeamReviewers}}
{{if .Team}}
<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}}>
<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
<span class="text">
{{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
</span>
<a class="item muted {{if .Requested}}checked{{end}}" href="#" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
{{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
<span class="item-check-mark">{{svg "octicon-check"}}</span>
{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
</a>
{{end}}
{{end}}
{{end}}
</div>
</div>
</div>
<div class="ui assignees list">
<span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
<div class="selected">
{{range .PullReviewers}}
<div class="item tw-flex tw-items-center tw-py-2">
<div class="tw-flex tw-items-center tw-flex-1">
<div class="ui relaxed list flex-items-block tw-my-4">
<span class="item empty-list {{if or $data.OriginalReviews $data.CurrentPullReviewers}}tw-hidden{{end}}">
{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}
</span>
{{range $data.CurrentPullReviewers}}
<div class="item">
<div class="flex-text-inline tw-flex-1">
{{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}}
<span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
{{end}}
</div>
<div class="tw-flex tw-items-center tw-gap-2">
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
<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}}">
<div class="flex-text-inline">
{{if .CanBeDismissed}}
<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}}
</a>
<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
{{end}}
{{if .Review.Stale}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">{{svg "octicon-hourglass" 16}}</span>
{{end}}
{{if and .CanChange $data.CanChooseReviewer}}
{{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}}
<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))}}
</span>
</div>
</div>
{{end}}
{{range $data.OriginalReviews}}
<div class="item">
<div class="flex-text-inline tw-flex-1">
{{$originalURLHostname := $pageMeta.Repository.GetOriginalURLHostname}}
{{$originalURL := $pageMeta.Repository.OriginalURL}}
<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>
</div>
<div class="flex-text-inline">
<span {{if .TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .TooltipContent}}"{{end}}>
{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
</span>
</div>
</div>
{{end}}
</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>
@ -68,12 +112,12 @@
<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}}">
<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="message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
<input id="message" name="message">
<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>
@ -83,34 +127,4 @@
</div>
</div>
{{end}}
{{if .Review.Stale}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">
{{svg "octicon-hourglass" 16}}
</span>
{{end}}
{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
<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>
{{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))}}
</span>
</div>
</div>
{{end}}
{{range .OriginalReviews}}
<div class="item tw-flex tw-items-center tw-py-2">
<div class="tw-flex tw-items-center tw-flex-1">
<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}}
{{.OriginalAuthor}}
</a>
</div>
<div class="tw-flex tw-items-center tw-gap-2">
<span {{if .TooltipContent}}data-tooltip-content="{{ctx.Locale.Tr .TooltipContent}}"{{end}}>
{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
</span>
</div>
</div>
{{end}}
</div>
</div>

View File

@ -1,5 +1,5 @@
{{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">
{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
</a>

View File

@ -2,17 +2,19 @@
{{template "repo/issue/branch_selector_field" $}}
{{if .Issue.IsPull}}
{{template "repo/issue/sidebar/reviewer_list" $}}
{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/wip_switch" $}}
<div class="divider"></div>
{{end}}
{{template "repo/issue/labels/labels_selector_field" $}}
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
{{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/watch_notification" $}}
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}

View File

@ -20095,6 +20095,20 @@
"format": "int64",
"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": {
"type": "string",
"x-go-name": "Title"

View File

@ -145,7 +145,8 @@ func TestPullRequestTargetEvent(t *testing.T) {
BaseRepo: baseRepo,
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)
// load and compare ActionRun
@ -199,7 +200,8 @@ func TestPullRequestTargetEvent(t *testing.T) {
BaseRepo: baseRepo,
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)
// the new pull request cannot trigger actions, so there is still only 1 record

View File

@ -11,6 +11,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
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"
"code.gitea.io/gitea/models/unittest"
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)
// 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)
reviewsCountCheck(t,

View File

@ -520,7 +520,8 @@ func TestConflictChecking(t *testing.T) {
BaseRepo: baseRepo,
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)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})

View File

@ -173,7 +173,8 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
BaseRepo: baseRepo,
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)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})

View File

@ -1388,6 +1388,7 @@ table th[data-sortt-desc] .svg {
align-items: stretch;
}
.ui.list.flex-items-block > .item,
.flex-items-block > .item,
.flex-text-block {
display: flex;

View File

@ -50,9 +50,20 @@
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 {
max-width: 270px;
min-width: 0;
max-height: 500px;
overflow-x: auto;
}
@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 {
max-height: 500px;
overflow-x: auto;
@ -118,10 +112,6 @@
left: 0;
}
.repository .select-label .desc {
padding-left: 23px;
}
/* For the secondary pointing menu, respect its own border-bottom */
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
@ -2463,12 +2453,6 @@ tbody.commit-list {
margin-top: 1em;
}
.sidebar-item-link {
display: inline-flex;
align-items: center;
overflow-wrap: anywhere;
}
.diff-file-header {
padding: 5px 8px !important;
box-shadow: 0 -1px 0 1px var(--color-body); /* prevent borders being visible behind top corners when sticky and scrolled */

View File

@ -47,6 +47,7 @@
}
.archived-label-hint {
float: right;
margin: -12px;
position: absolute;
top: 10px;
right: 5px;
}

View File

@ -32,13 +32,13 @@ export function initGlobalDropdown() {
const $uiDropdowns = fomanticQuery('.ui.dropdown');
// 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,
// clicking an item will jump to somewhere else or trigger an action/function.
// 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.
$uiDropdowns.filter('.jump').dropdown({
$uiDropdowns.filter('.jump').dropdown('setting', {
action: 'hide',
onShow() {
// hide associated tooltip while dropdown is open

View 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();
}

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

View File

@ -1,29 +1,7 @@
import $ from 'jquery';
import {POST} from '../modules/fetch.ts';
import {updateIssuesMeta} from './repo-common.ts';
import {svg} from '../svg.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();
}
import {queryElems, toggleElem} from '../utils/dom.ts';
import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
function initBranchSelector() {
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
@ -47,218 +25,12 @@ function initBranchSelector() {
} else {
// for new issue, only update UI&form, do not send request/reload
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;
}
});
}
// 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() {
const form = document.querySelector<HTMLFormElement>('.issue-due-form');
if (!form) return;
@ -276,14 +48,6 @@ export function initRepoIssueSidebar() {
initBranchSelector();
initRepoIssueDue();
// Init labels and assignees
initListSubmits('select-label', 'labels');
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');
// init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
}

View File

@ -8,7 +8,6 @@ import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
import {updateIssuesMeta} from './repo-common.ts';
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) => {
if (e.altKey && e.key === 'Enter') {
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() {
@ -326,17 +325,6 @@ export function initRepoIssueWipTitle() {
export function initRepoIssueComments() {
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) => {
const urlTarget = document.querySelector(':target');
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() {
const $commentForm = $('.comment.form');
if (!$commentForm.length) return;

View File

@ -30,7 +30,7 @@ import {
initRepoIssueWipTitle,
initRepoPullRequestMergeInstruction,
initRepoPullRequestAllowMaintainerEdit,
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
initRepoPullRequestReview, initRepoIssueSidebarList,
} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
@ -182,7 +182,6 @@ onDomReady(() => {
initRepoIssueContentHistory,
initRepoIssueList,
initRepoIssueSidebarList,
initArchivedLabelHandler,
initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking,
initRepoIssueWipTitle,

View File

@ -57,10 +57,21 @@ export async function renderMermaid() {
btn.setAttribute('data-clipboard-text', source);
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', () => {
pre.replaceWith(mermaidBlock);
mermaidBlock.classList.remove('tw-hidden');
iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
updateIframeHeight();
setTimeout(() => { // avoid flash of iframe background
mermaidBlock.classList.remove('is-loading');
iframe.classList.remove('tw-invisible');

View File

@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest';
import type $ from 'jquery';
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 ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
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) {
for (const el of elems) {
fn(el);
@ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?:
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[];
return applyElemsCallback<T>(elems.filter((child: Element) => {
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
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);
}
// it works like parent.querySelectorAll: all descendants are selected
// 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);
}