}
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
}
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})
}
}
-// 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 {
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"))
"ctx": func() any { return nil }, // template context function
"DumpVar": dumpVar,
+ "NIL": func() any { return nil },
// -----------------------------------------------------------------
// html/template related functions
if !nothingToCompare {
// Setup information for new form.
- retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, true)
+ pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, true)
if ctx.Written() {
return
}
- labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, true)
- if ctx.Written() {
- return
- }
- RetrieveRepoReviewers(ctx, ctx.Repo.Repository, nil, true)
- if ctx.Written() {
- return
- }
- _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, labelsData)
+ _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
}
return 0
}
- retrieveProjects(ctx, repo)
+ retrieveProjectsForIssueList(ctx, repo)
if ctx.Written() {
return
}
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
})
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,
})
if err != nil {
ctx.ServerError("GetProjects", err)
- return
+ return nil, nil
}
}
})
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{
})
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
}
type issueSidebarReviewersData struct {
- Repository *repo_model.Repository
- RepoOwnerName string
- RepoLink string
- IssueID int64
CanChooseReviewer bool
OriginalReviews issues_model.ReviewList
TeamReviewers []*repoReviewerSelection
}
// RetrieveRepoReviewers find all reviewers of a repository. If issue is nil, it means the doer is creating a new PR.
-func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
- data := &issueSidebarReviewersData{}
- data.RepoLink = ctx.Repo.RepoLink
- data.Repository = repo
- data.RepoOwnerName = repo.OwnerName
- data.CanChooseReviewer = canChooseReviewer
+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 issue == nil {
+ if d.Issue == nil {
posterID = ctx.Doer.ID
} else {
- posterID = issue.PosterID
- if issue.OriginalAuthorID > 0 {
+ posterID = d.Issue.PosterID
+ if d.Issue.OriginalAuthorID > 0 {
posterID = 0 // for migrated PRs, no poster ID
}
- data.IssueID = issue.ID
- isClosed = issue.IsClosed || issue.PullRequest.HasMerged
+ isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged
- originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID)
+ originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID)
if err != nil {
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
return
}
data.OriginalReviews = originalAuthorReviews
- reviews, err = issues_model.GetReviewsByIssueID(ctx, 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
}
}
reviewers []*user_model.User
)
- if canChooseReviewer {
+ if data.CanChooseReviewer {
var err error
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
if err != nil {
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 {
pullReviews = append(pullReviews, tmp)
- if canChooseReviewer {
+ if data.CanChooseReviewer {
if tmp.IsTeam {
teamReviewersResult = append(teamReviewersResult, tmp)
} else {
data.CurrentPullReviewers = currentPullReviewers
}
- if canChooseReviewer && reviewersResult != nil {
+ if data.CanChooseReviewer && reviewersResult != nil {
preadded := len(reviewersResult)
for _, reviewer := range reviewers {
found := false
data.Reviewers = reviewersResult
}
- if canChooseReviewer && teamReviewersResult != nil {
+ if data.CanChooseReviewer && teamReviewersResult != nil {
preadded := len(teamReviewersResult)
for _, team := range teamReviewers {
found := false
data.TeamReviewers = teamReviewersResult
}
-
- ctx.Data["IssueSidebarReviewersData"] = data
}
type issueSidebarLabelsData struct {
- Repository *repo_model.Repository
- RepoLink string
- IssueID int64
- IsPullRequest bool
AllLabels []*issues_model.Label
RepoLabels []*issues_model.Label
OrgLabels []*issues_model.Label
)
}
-func retrieveRepoLabels(ctx *context.Context, repo *repo_model.Repository, issueID int64, isPull bool) *issueSidebarLabelsData {
- labelsData := &issueSidebarLabelsData{
- Repository: repo,
- RepoLink: ctx.Repo.RepoLink,
- IssueID: issueID,
- IsPullRequest: isPull,
- }
- ctx.Data["IssueSidebarLabelsData"] = labelsData
+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
}
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
}
labelsData.OrgLabels = orgLabels
}
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
- return labelsData
-}
-
-// retrieveRepoMetasForIssueWriter finds some the meta information of a repository for an issue/pr writer
-func retrieveRepoMetasForIssueWriter(ctx *context.Context, repo *repo_model.Repository, isPull bool) {
- if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
- return
- }
-
- RetrieveRepoMilestonesAndAssignees(ctx, repo)
- if ctx.Written() {
- return
- }
-
- retrieveProjects(ctx, repo)
- if ctx.Written() {
- return
- }
-
- PrepareBranchList(ctx)
- if ctx.Written() {
- return
- }
- // Contains true if the user can create issue dependencies
- ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
}
// 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, labelsData *issueSidebarLabelsData) (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
ctx.Data["TemplateFile"] = template.FileName
}
- labelsData.SetSelectedLabelNames(template.Labels)
+ metaData.LabelsData.SetSelectedLabelNames(template.Labels)
- selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
- if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
+ if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, true); err == nil {
for _, userID := range userIDs {
- 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["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
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
- }
+ pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, false)
+ if ctx.Written() {
+ return
}
- 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.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"
}
}
- retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, false)
- if ctx.Written() {
- return
- }
- labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, false)
- if ctx.Written() {
- return
- }
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
ctx.Data["Tags"] = tags
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, labelsData)
+ templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
for k, v := range errs {
ret.TemplateErrors[k] = v
}
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) (ret struct {
+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
+}
+
+// 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
TeamReviewers []*organization.Team
},
) {
- var (
- repo = ctx.Repo.Repository
- err error
- )
-
- retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, isPull)
- if ctx.Written() {
- return ret
- }
- labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, isPull)
+ pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, isPull)
if ctx.Written() {
return ret
}
- var labelIDs []int64
- // Check labels.
- if len(form.LabelIDs) > 0 {
- labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
- if err != nil {
- return ret
- }
- labelsData.SetSelectedLabelIDs(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 ret
- }
- if milestone.RepoID != repo.ID {
- ctx.ServerError("GetMilestoneByID", err)
- return ret
- }
- ctx.Data["Milestone"] = milestone
- ctx.Data["milestone_id"] = milestoneID
+ 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 ret
}
+ pageMetaData.LabelsData.SetSelectedLabelIDs(inputLabelIDs)
- if form.ProjectID > 0 {
- p, err := project_model.GetProjectByID(ctx, form.ProjectID)
- if err != nil {
- ctx.ServerError("GetProjectByID", err)
- return ret
- }
- if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
- ctx.NotFound("", nil)
- return ret
- }
-
- 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 ret
- }
-
- // 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 ret
- }
-
- valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
- if err != nil {
- ctx.ServerError("CanBeAssigned", err)
- return ret
- }
-
- if !valid {
- ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
- return ret
- }
- }
+ 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
- // Keep the old assignee id thingy for compatibility reasons
- if form.AssigneeID > 0 {
- assigneeIDs = append(assigneeIDs, form.AssigneeID)
+ candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
+ inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
+ if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) {
+ ctx.NotFound("", nil)
+ return ret
}
+ pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs
- // Check reviewers
+ // Check if the passed reviewers (user/team) actually exist
var reviewers []*user_model.User
var teamReviewers []*organization.Team
- if isPull && len(form.ReviewerIDs) > 0 {
- reviewerIDs, err := base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
- if err != nil {
- return ret
+ reviewerIDs, _ := base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
+ if isPull && len(reviewerIDs) > 0 {
+ userReviewersMap := map[int64]*user_model.User{}
+ teamReviewersMap := map[int64]*organization.Team{}
+ for _, r := range pageMetaData.ReviewersData.Reviewers {
+ userReviewersMap[r.User.ID] = r.User
+ }
+ for _, r := range pageMetaData.ReviewersData.TeamReviewers {
+ teamReviewersMap[r.Team.ID] = r.Team
}
- // Check if the passed reviewers (user/team) actually exist
for _, rID := range reviewerIDs {
- // negative reviewIDs represent team requests
- if rID < 0 {
- teamReviewer, err := organization.GetTeamByID(ctx, -rID)
- if err != nil {
- ctx.ServerError("GetTeamByID", err)
+ if rID < 0 { // negative reviewIDs represent team requests
+ team, ok := teamReviewersMap[-rID]
+ if !ok {
+ ctx.NotFound("", nil)
return ret
}
- teamReviewers = append(teamReviewers, teamReviewer)
- continue
- }
-
- reviewer, err := user_model.GetUserByID(ctx, rID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return ret
+ teamReviewers = append(teamReviewers, team)
+ } else {
+ user, ok := userReviewersMap[rID]
+ if !ok {
+ ctx.NotFound("", nil)
+ return ret
+ }
+ reviewers = append(reviewers, user)
}
- reviewers = append(reviewers, reviewer)
}
}
- ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = 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
}
attachments []string
)
- validateRet := ValidateRepoMetas(ctx, *form, false)
+ validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false)
if ctx.Written() {
return
}
}
}
- retrieveRepoMetasForIssueWriter(ctx, repo, issue.IsPull)
+ pageMetaData := retrieveRepoIssueMetaData(ctx, repo, issue, issue.IsPull)
if ctx.Written() {
return
}
- labelsData := retrieveRepoLabels(ctx, repo, issue.ID, issue.IsPull)
- if ctx.Written() {
- return
- }
- labelsData.SetSelectedLabels(issue.Labels)
-
- // Check milestone and assignee.
- if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
- RetrieveRepoMilestonesAndAssignees(ctx, repo)
- retrieveProjects(ctx, repo)
-
- if ctx.Written() {
- return
- }
- }
-
- if issue.IsPull {
- canChooseReviewer := false
- if ctx.Doer != nil && ctx.IsSigned {
- canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
- }
-
- RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
- if ctx.Written() {
- return
- }
- }
+ pageMetaData.LabelsData.SetSelectedLabels(issue.Labels)
if ctx.IsSigned {
// Update issue-user.
return
}
- validateRet := ValidateRepoMetas(ctx, *form, true)
+ validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
if ctx.Written() {
return
}
Ref string `form:"ref"`
MilestoneID int64
ProjectID int64
- AssigneeID int64
Content string
Files []string
AllowMaintainerEdit bool
+++ /dev/null
-{{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}}
<div class="issue-content-right ui segment">
{{template "repo/issue/branch_selector_field" $}}
{{if .PageIsComparePull}}
- {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
<div class="divider"></div>
{{end}}
- {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
-
- <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>
-
- <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>
+ {{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}
{{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>
+ {{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
+
{{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 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>
{{end}}
</div>
+{{$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}}
- </a>
- <div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
- <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 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="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="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
+ {{range $data.CandidateAssignees}}
+ <a class="item muted" href="#" data-value="{{.ID}}">
+ <span class="item-check-mark">{{svg "octicon-check"}}</span>
+ {{ctx.AvatarUtils.Avatar . 20}} {{template "repo/search_name" .}}
+ </a>
+ {{end}}
</div>
- <div 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}}
- {{end}}
- {{end}}
- <span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
- <span class="text">
- {{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
- </span>
- </a>
- {{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>
+ <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>
-{{$data := .}}
-{{$canChange := and ctx.RootData.HasIssuesOrPullsWritePermission (not $data.Repository.IsArchived)}}
-<div class="issue-sidebar-combo" {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/labels?issue_ids={{$data.IssueID}}"{{end}}>
+{{$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 $canChange}}disabled{{end}}">
+ <div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted">
- <strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $canChange}}{{svg "octicon-gear"}}{{end}}
+ <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>
<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
{{$previousExclusiveScope := "_no_scope"}}
- {{range .RepoLabels}}
+ {{range $data.RepoLabels}}
{{$exclusiveScope := .ExclusiveScope}}
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
<div class="divider"></div>
{{end}}
<div class="divider"></div>
{{$previousExclusiveScope = "_no_scope"}}
- {{range .OrgLabels}}
+ {{range $data.OrgLabels}}
{{$exclusiveScope := .ExclusiveScope}}
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
<div class="divider"></div>
<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="{{$data.RepoLink}}/{{if $data.IsPullRequest}}pulls{{else}}issues{{end}}?labels={{.ID}}">
+ <a class="item" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?labels={{.ID}}">
{{- ctx.RenderUtils.RenderLabel . -}}
</a>
{{end}}
{{$label := .Label}}
-<a class="item {{if $label.IsChecked}}checked{{else if $label.IsArchived}}tw-hidden{{end}}" href="#"
+<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>
+{{$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}}
- </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="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">
+ {{if and (not $data.OpenMilestones) (not $data.ClosedMilestones)}}
+ <div class="item disabled">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
+ {{else}}
+ <div class="ui icon search input">
+ <i class="icon">{{svg "octicon-search"}}</i>
+ <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestones"}}">
+ </div>
+ <div class="divider"></div>
+ <div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
+ {{if $data.OpenMilestones}}
+ <div class="divider"></div>
+ <div class="header">{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}</div>
+ {{range $data.OpenMilestones}}
+ <a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}">
+ {{svg "octicon-milestone" 18}} {{.Name}}
+ </a>
+ {{end}}
+ {{end}}
+ {{if $data.ClosedMilestones}}
+ <div class="divider"></div>
+ <div class="header">{{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}</div>
+ {{range $data.ClosedMilestones}}
+ <a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}">
+ {{svg "octicon-milestone" 18}} {{.Name}}
+ </a>
+ {{end}}
+ {{end}}
+ {{end}}
+ </div>
</div>
-</div>
-<div 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="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>
<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>
-{{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}}
- </a>
- {{end}}
- </div>
+ <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>
-{{end}}
+</div>
-{{$data := .}}
+{{$pageMeta := .}}
+{{$data := .ReviewersData}}
+{{$repoOwnerName := $pageMeta.Repository.OwnerName}}
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
-<div class="issue-sidebar-combo" {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/request_review?issue_ids={{$data.IssueID}}"{{end}}>
+<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 and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
+ <strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu flex-items-menu">
{{if $hasCandidates}}
<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}} {{$data.RepoOwnerName}}/{{.Team.Name}}
+ {{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
</a>
{{end}}
{{end}}
{{if .User}}
<a class="muted flex-text-inline" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}</a>
{{else if .Team}}
- {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
+ {{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
{{end}}
</div>
<div class="flex-text-inline">
{{if .Requested}}
<a href="#" class="ui muted icon link-action"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review"}}"
- data-url="{{$data.RepoLink}}/issues/request_review?action=detach&issue_ids={{$data.IssueID}}&id={{.ItemID}}">
+ 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="{{$data.RepoLink}}/issues/request_review?action=attach&issue_ids={{$data.IssueID}}&id={{.ItemID}}">
+ data-url="{{$pageMeta.RepoLink}}/issues/request_review?action=attach&issue_ids={{$pageMeta.Issue.ID}}&id={{.ItemID}}">
{{svg "octicon-sync"}}
</a>
{{end}}
{{range $data.OriginalReviews}}
<div class="item">
<div class="flex-text-inline tw-flex-1">
- {{$originalURLHostname := $data.Repository.GetOriginalURLHostname}}
- {{$originalURL := $data.Repository.OriginalURL}}
+ {{$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 class="ui warning message">
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
</div>
- <form class="ui form" action="{{$data.RepoLink}}/issues/dismiss_review" method="post">
+ <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">
{{template "repo/issue/branch_selector_field" $}}
{{if .Issue.IsPull}}
- {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/wip_switch" $}}
<div class="divider"></div>
{{end}}
- {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
+ {{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" $}}
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 */
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
// if there are draft comments, confirm before reloading, to avoid losing comments
-export function issueSidebarReloadConfirmDraftComment() {
+function issueSidebarReloadConfirmDraftComment() {
const commentTextareas = [
document.querySelector<HTMLTextAreaElement>('.edit-content-zone:not(.tw-hidden) textarea'),
document.querySelector<HTMLTextAreaElement>('#comment-form textarea'),
window.location.reload();
}
-function collectCheckedValues(elDropdown: HTMLElement) {
- return Array.from(elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
-}
+class IssueSidebarComboList {
+ updateUrl: string;
+ updateAlgo: string;
+ selectionMode: string;
+ elDropdown: HTMLElement;
+ elList: HTMLElement;
+ elComboValue: HTMLInputElement;
+ initialValues: string[];
-export function initIssueSidebarComboList(container: HTMLElement) {
- const updateUrl = container.getAttribute('data-update-url');
- const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
- const elList = container.querySelector<HTMLElement>(':scope > .ui.list');
- const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
- let initialValues = collectCheckedValues(elDropdown);
+ 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');
+ }
+ }
- elDropdown.addEventListener('click', (e) => {
+ 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(elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
- elComboValue.value = '';
+ 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 = elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
+ const elSelected = this.elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
if (elSelected === elItem) {
elItem.classList.toggle('checked');
} else {
- queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
+ queryElems(this.elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
elItem.classList.toggle('checked', true);
}
} else {
- elItem.classList.toggle('checked');
- }
- elComboValue.value = collectCheckedValues(elDropdown).join(',');
- });
-
- const updateToBackend = async (changedValues) => {
- let changed = false;
- for (const value of initialValues) {
- if (!changedValues.includes(value)) {
- await POST(updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
- changed = true;
+ 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);
}
}
- for (const value of changedValues) {
- if (!initialValues.includes(value)) {
- await POST(updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
- changed = 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);
}
- if (changed) issueSidebarReloadConfirmDraftComment();
- };
+ this.initialValues = this.collectCheckedValues();
- const syncUiList = (changedValues) => {
- const elEmptyTip = elList.querySelector('.item.empty-list');
- queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
- for (const value of changedValues) {
- const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
- const listItem = el.cloneNode(true) as HTMLElement;
- queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
- elList.append(listItem);
- }
- const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
- toggleElem(elEmptyTip, !hasItems);
- };
-
- fomanticQuery(elDropdown).dropdown('setting', {
- action: 'nothing', // do not hide the menu if user presses Enter
- fullTextSearch: 'exact',
- async onHide() {
- // TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
- const changedValues = collectCheckedValues(elDropdown);
- syncUiList(changedValues);
- if (updateUrl) await updateToBackend(changedValues);
- initialValues = changedValues;
- },
- });
+ 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();
}
A sidebar combo (dropdown+list) is like this:
```html
-<div class="issue-sidebar-combo" data-update-url="...">
+<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">
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.
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 {queryElems, toggleElem} from '../utils/dom.ts';
-import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
+import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
function initBranchSelector() {
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
});
}
-// 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) {
- issueSidebarReloadConfirmDraftComment();
- }
- }
- },
- });
-
- $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;
- }
- if (this !== clickedItem && !this.classList.contains('checked')) {
- return;
- }
- } else if (this !== clickedItem) {
- // Toggle for other labels
- return;
- }
-
- 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-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'),
- '',
- );
- issueSidebarReloadConfirmDraftComment();
- })();
- }
-
- $(this).parent().find('.item').each(function () {
- $(this).removeClass('checked');
- $(this).find('.octicon-check').addClass('tw-invisible');
- });
-
- if (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'),
- );
- issueSidebarReloadConfirmDraftComment();
- })();
- }
-
- 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'),
- );
- issueSidebarReloadConfirmDraftComment();
- })();
- }
-
- $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;
initBranchSelector();
initRepoIssueDue();
- // TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
- initListSubmits('select-assignees', 'assignees');
- initListSubmits('select-assignees-modify', 'assignees');
- selectItem('.select-assignee', '#assignee_id');
-
- selectItem('.select-project', '#project_id');
- selectItem('.select-milestone', '#milestone_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));
}