]> source.dussan.org Git - gitea.git/commitdiff
Refactor sidebar assignee&milestone&project selectors (#32465)
authorwxiaoguang <wxiaoguang@gmail.com>
Sun, 10 Nov 2024 20:07:54 +0000 (04:07 +0800)
committerGitHub <noreply@github.com>
Sun, 10 Nov 2024 20:07:54 +0000 (04:07 +0800)
Follow #32460

Now the code could be much clearer than before and easier to maintain. A
lot of legacy code is removed.

Manually tested.

This PR is large enough, that fine tunes could be deferred to the future if
there is no bug found or design problem.

Screenshots:

<details>

![image](https://github.com/user-attachments/assets/35f4ab7b-1bc0-4bad-a73c-a4569328303c)

</details>

23 files changed:
modules/base/tool.go
modules/base/tool_test.go
modules/container/set.go
modules/container/set_test.go
modules/templates/helper.go
routers/web/repo/compare.go
routers/web/repo/issue.go
routers/web/repo/pull.go
services/forms/repo_form.go
templates/repo/issue/milestone/select_menu.tmpl [deleted file]
templates/repo/issue/new_form.tmpl
templates/repo/issue/sidebar/assignee_list.tmpl
templates/repo/issue/sidebar/label_list.tmpl
templates/repo/issue/sidebar/label_list_item.tmpl
templates/repo/issue/sidebar/milestone_list.tmpl
templates/repo/issue/sidebar/participant_list.tmpl
templates/repo/issue/sidebar/project_list.tmpl
templates/repo/issue/sidebar/reviewer_list.tmpl
templates/repo/issue/view_content/sidebar.tmpl
web_src/css/repo.css
web_src/js/features/repo-issue-sidebar-combolist.ts
web_src/js/features/repo-issue-sidebar.md
web_src/js/features/repo-issue-sidebar.ts

index 9e43030f400192199b39fce97845f23a988a70df..928c80700b828042174e3e25cba68981b0fc084a 100644 (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
index 4af8b9bc4d5285a62016c2dc39e171427ac2c090..86cccdf2092d9b62652695a20bc581a7992cdf48 100644 (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})
 
index adb77dcac7a7a2a795b6e4adc585e17442bfc3b4..105533f203336ef835bc890c5f368dd09bcc4acf 100644 (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 {
index 1502236034a13b20183fd274b65bda63b991521d..a8b7ff819086320e67a288407bcc854265c20448 100644 (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"))
index efaa10624bd87c829aaf71aa335becf5d82f1d44..3ef11772dc7d7c542de442b834199c2f02a1f028 100644 (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
index 9a7d3dfbf653c8552b16ffaf68cc1ea3d751fceb..a5fdba3fde696a668324e2a36b1e88b003175f23 100644 (file)
@@ -788,19 +788,11 @@ func CompareDiff(ctx *context.Context) {
 
                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)
                        }
index a4e2fd8cea310b83171fc508d127e25e5d0b11b1..72f89bd27d0a048129b35210909d3aab6f46036f 100644 (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
@@ -665,10 +773,6 @@ type repoReviewerSelection struct {
 }
 
 type issueSidebarReviewersData struct {
-       Repository           *repo_model.Repository
-       RepoOwnerName        string
-       RepoLink             string
-       IssueID              int64
        CanChooseReviewer    bool
        OriginalReviews      issues_model.ReviewList
        TeamReviewers        []*repoReviewerSelection
@@ -677,41 +781,44 @@ type issueSidebarReviewersData struct {
 }
 
 // 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
                }
        }
@@ -724,7 +831,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
                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 {
@@ -760,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 {
@@ -770,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 {
@@ -811,7 +918,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
                data.CurrentPullReviewers = currentPullReviewers
        }
 
-       if canChooseReviewer && reviewersResult != nil {
+       if data.CanChooseReviewer && reviewersResult != nil {
                preadded := len(reviewersResult)
                for _, reviewer := range reviewers {
                        found := false
@@ -839,7 +946,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
                data.Reviewers = reviewersResult
        }
 
-       if canChooseReviewer && teamReviewersResult != nil {
+       if data.CanChooseReviewer && teamReviewersResult != nil {
                preadded := len(teamReviewersResult)
                for _, team := range teamReviewers {
                        found := false
@@ -866,15 +973,9 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
 
                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
@@ -922,60 +1023,30 @@ func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
        )
 }
 
-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
@@ -1013,24 +1084,20 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
                        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
@@ -1057,42 +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
-               }
+       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)
@@ -1101,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, labelsData)
+       templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
        for k, v := range errs {
                ret.TemplateErrors[k] = v
        }
@@ -1196,8 +1240,16 @@ 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) (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
 
@@ -1205,126 +1257,76 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
        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
 }
@@ -1344,7 +1346,7 @@ func NewIssuePost(ctx *context.Context) {
                attachments []string
        )
 
-       validateRet := ValidateRepoMetas(ctx, *form, false)
+       validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false)
        if ctx.Written() {
                return
        }
@@ -1619,37 +1621,11 @@ func ViewIssue(ctx *context.Context) {
                }
        }
 
-       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.
index dd9671efbe8ad21e99f442a8b47b3a189c8a248b..bb814eab6e70d108994089559d82463f29fe66ac 100644 (file)
@@ -1269,7 +1269,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
                return
        }
 
-       validateRet := ValidateRepoMetas(ctx, *form, true)
+       validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
        if ctx.Written() {
                return
        }
index 83f2dd6caacfc698a7bf14d235299c6c2f6ab24e..d27bbca8948113be89f0ee14aa769924ab23c15f 100644 (file)
@@ -451,7 +451,6 @@ type CreateIssueForm struct {
        Ref                 string `form:"ref"`
        MilestoneID         int64
        ProjectID           int64
-       AssigneeID          int64
        Content             string
        Files               []string
        AllowMaintainerEdit bool
diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl
deleted file mode 100644 (file)
index 9b0492c..0000000
+++ /dev/null
@@ -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}}
index 65d359e9dcda68d3b9d205f66a27c4025cbf7758..ceaaebc4d5440cd5bec3b1c12f24c803de6dc957 100644 (file)
        <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>
index 260f7c5be4dae999a1992d7658ec3474d979d731..bee6123e52ce7b2dfc4e0efc1381d9b08613e7a3 100644 (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}}
-       </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>
index e9f4baa43354a55e09cd471bfa5f04816cae75f7..ed80047661bc0c8dc60b3afb4b82d4f21a59f40b 100644 (file)
@@ -1,10 +1,12 @@
-{{$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}}
@@ -16,7 +18,7 @@
                                </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>
@@ -26,7 +28,7 @@
                                {{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>
@@ -42,7 +44,7 @@
                <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}}
index ad878e918be161353523f276770575cee28506fe..5c6808d95b21e4ce4797ce21bb3e1151d6de8a23 100644 (file)
@@ -1,5 +1,5 @@
 {{$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>
index e9ca02f77a15a2343771d28b188fdc40be365481..4f2b4cb06fb21c1916f6bf80c93143c1eef2b849 100644 (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}}
-       </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>
index 91c36fc01ef8d89871c63f9074ab746e69abfabf..11debf95c48e9f613cc82600ef4f42a8b624b8cd 100644 (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>
index ec79f8032f7b0b819ea06c542a97533a1b6acabb..ab1243caddcb22a1a0a1794e14934ef08d11f6f6 100644 (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}}
-                               </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>
index cf7b97c02b194041e9e409d09b467f8516d79908..e990fc5afc87de2a76a08a460979597e03d12275 100644 (file)
@@ -1,10 +1,14 @@
-{{$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}}
@@ -29,7 +33,7 @@
                                                <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}}
@@ -47,7 +51,7 @@
                                        {{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}}
@@ -84,8 +88,8 @@
                {{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">
index 0fae1e9e1c1a50b43e53b12750441e609a40dabb..02f5d3e2df90fc4d138064fec974e8c8fc9b30d4 100644 (file)
@@ -2,16 +2,19 @@
        {{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" $}}
index ff8342d29aae78e4dae5faf7a649e77ec070c09a..01ddab97e595c3afdd4de71552f75b592da4a2a1 100644 (file)
@@ -2453,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 */
index f408eb43ba0d866eb90e77ef7a7727b2507fbf0a..24d620547f7f59c50ca82472b03382dfe4e1db64 100644 (file)
@@ -3,7 +3,7 @@ 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
-export function issueSidebarReloadConfirmDraftComment() {
+function issueSidebarReloadConfirmDraftComment() {
   const commentTextareas = [
     document.querySelector<HTMLTextAreaElement>('.edit-content-zone:not(.tw-hidden) textarea'),
     document.querySelector<HTMLTextAreaElement>('#comment-form textarea'),
@@ -22,84 +22,138 @@ export function issueSidebarReloadConfirmDraftComment() {
   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();
 }
index 3022b52d05becc8659e02af0f8bac933ff0663f1..6de013f1c2833f6976d3d29a10f7dfcb6c22f592 100644 (file)
@@ -1,7 +1,7 @@
 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">
@@ -25,3 +25,7 @@ If there is `data-update-url`, it also calls backend to attach/detach the change
 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.
index 52878848e8cb3b09af8c52effe2b6fb48cd4933f..45cd38d533d33be09fe0879e06614eb29c66ac59 100644 (file)
@@ -1,10 +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 {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');
@@ -34,212 +31,6 @@ function initBranchSelector() {
   });
 }
 
-// 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;
@@ -257,14 +48,6 @@ export function initRepoIssueSidebar() {
   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));
 }