diff --git a/modules/base/tool.go b/modules/base/tool.go
index 9e43030f400..928c80700b8 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -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
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 4af8b9bc4d5..86cccdf2092 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -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})
 
diff --git a/modules/container/set.go b/modules/container/set.go
index adb77dcac7a..105533f2033 100644
--- a/modules/container/set.go
+++ b/modules/container/set.go
@@ -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 {
diff --git a/modules/container/set_test.go b/modules/container/set_test.go
index 1502236034a..a8b7ff81908 100644
--- a/modules/container/set_test.go
+++ b/modules/container/set_test.go
@@ -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"))
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index efaa10624bd..3ef11772dc7 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -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
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 9a7d3dfbf65..a5fdba3fde6 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -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)
 			}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a4e2fd8cea3..72f89bd27d0 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -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)
+	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)
 
-	// 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
+	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
 
-	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
+	allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
+	candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
+	if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) {
+		ctx.NotFound("", nil)
+		return ret
 	}
+	pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
 
-	// Check 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
-			}
-		}
+	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
 
-	// Keep the old assignee id thingy for compatibility reasons
-	if form.AssigneeID > 0 {
-		assigneeIDs = append(assigneeIDs, form.AssigneeID)
-	}
-
-	// 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
+				teamReviewers = append(teamReviewers, team)
+			} else {
+				user, ok := userReviewersMap[rID]
+				if !ok {
+					ctx.NotFound("", nil)
+					return ret
+				}
+				reviewers = append(reviewers, user)
 			}
-
-			reviewer, err := user_model.GetUserByID(ctx, rID)
-			if err != nil {
-				ctx.ServerError("GetUserByID", err)
-				return ret
-			}
-			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.
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index dd9671efbe8..bb814eab6e7 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1269,7 +1269,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 		return
 	}
 
-	validateRet := ValidateRepoMetas(ctx, *form, true)
+	validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
 	if ctx.Written() {
 		return
 	}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 83f2dd6caac..d27bbca8948 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -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
index 9b0492ce524..00000000000
--- a/templates/repo/issue/milestone/select_menu.tmpl
+++ /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}}
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 65d359e9dcd..ceaaebc4d54 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -49,142 +49,22 @@
 	<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>
diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl
index 260f7c5be4d..bee6123e52c 100644
--- a/templates/repo/issue/sidebar/assignee_list.tmpl
+++ b/templates/repo/issue/sidebar/assignee_list.tmpl
@@ -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>
+	</div>
+	<div class="ui list tw-flex tw-flex-row tw-gap-2">
+		<span class="item empty-list {{if $issueAssignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
+		{{range $issueAssignees}}
+			<a class="item muted" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
+					{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
 			</a>
 		{{end}}
 	</div>
 </div>
-<div class="ui assignees list">
-	<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
-	<div class="selected">
-		{{range .Issue.Assignees}}
-			<div class="item">
-				<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
-					{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
-					{{.GetDisplayName}}
-				</a>
-			</div>
-		{{end}}
-	</div>
-</div>
diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl
index e9f4baa4335..ed80047661b 100644
--- a/templates/repo/issue/sidebar/label_list.tmpl
+++ b/templates/repo/issue/sidebar/label_list.tmpl
@@ -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}}
diff --git a/templates/repo/issue/sidebar/label_list_item.tmpl b/templates/repo/issue/sidebar/label_list_item.tmpl
index ad878e918be..5c6808d95b2 100644
--- a/templates/repo/issue/sidebar/label_list_item.tmpl
+++ b/templates/repo/issue/sidebar/label_list_item.tmpl
@@ -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>
diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl
index e9ca02f77a1..4f2b4cb06fb 100644
--- a/templates/repo/issue/sidebar/milestone_list.tmpl
+++ b/templates/repo/issue/sidebar/milestone_list.tmpl
@@ -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>
diff --git a/templates/repo/issue/sidebar/participant_list.tmpl b/templates/repo/issue/sidebar/participant_list.tmpl
index 91c36fc01ef..11debf95c48 100644
--- a/templates/repo/issue/sidebar/participant_list.tmpl
+++ b/templates/repo/issue/sidebar/participant_list.tmpl
@@ -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>
diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl
index ec79f8032f7..ab1243caddc 100644
--- a/templates/repo/issue/sidebar/project_list.tmpl
+++ b/templates/repo/issue/sidebar/project_list.tmpl
@@ -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>
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl
index cf7b97c02b1..e990fc5afc8 100644
--- a/templates/repo/issue/sidebar/reviewer_list.tmpl
+++ b/templates/repo/issue/sidebar/reviewer_list.tmpl
@@ -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">
@@ -64,13 +68,13 @@
 						{{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>
@@ -108,7 +112,7 @@
 			<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">
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 0fae1e9e1c1..02f5d3e2df9 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -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" $}}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index ff8342d29aa..01ddab97e59 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -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 */
diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts
index f408eb43ba0..24d620547f7 100644
--- a/web_src/js/features/repo-issue-sidebar-combolist.ts
+++ b/web_src/js/features/repo-issue-sidebar-combolist.ts
@@ -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');
+  }
 
-  elDropdown.addEventListener('click', (e) => {
+  collectCheckedValues() {
+    return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
+  }
+
+  updateUiList(changedValues) {
+    const elEmptyTip = this.elList.querySelector('.item.empty-list');
+    queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
+    for (const value of changedValues) {
+      const el = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
+      if (!el) continue;
+      const listItem = el.cloneNode(true) as HTMLElement;
+      queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
+      this.elList.append(listItem);
+    }
+    const hasItems = Boolean(this.elList.querySelector('.item:not(.empty-list)'));
+    toggleElem(elEmptyTip, !hasItems);
+  }
+
+  async updateToBackend(changedValues) {
+    if (this.updateAlgo === 'diff') {
+      for (const value of this.initialValues) {
+        if (!changedValues.includes(value)) {
+          await POST(this.updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
+        }
+      }
+      for (const value of changedValues) {
+        if (!this.initialValues.includes(value)) {
+          await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
+        }
+      }
+    } else {
+      await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
+    }
+    issueSidebarReloadConfirmDraftComment();
+  }
+
+  async doUpdate() {
+    const changedValues = this.collectCheckedValues();
+    if (this.initialValues.join(',') === changedValues.join(',')) return;
+    this.updateUiList(changedValues);
+    if (this.updateUrl) await this.updateToBackend(changedValues);
+    this.initialValues = changedValues;
+  }
+
+  async onChange() {
+    if (this.selectionMode === 'single') {
+      await this.doUpdate();
+      fomanticQuery(this.elDropdown).dropdown('hide');
+    }
+  }
+
+  async onItemClick(e) {
     const elItem = (e.target as HTMLElement).closest('.item');
     if (!elItem) return;
     e.preventDefault();
     if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
 
     if (elItem.matches('.clear-selection')) {
-      queryElems(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);
-  };
+    this.elDropdown.addEventListener('click', (e) => this.onItemClick(e));
 
-  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;
-    },
-  });
+    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();
 }
diff --git a/web_src/js/features/repo-issue-sidebar.md b/web_src/js/features/repo-issue-sidebar.md
index 3022b52d05b..6de013f1c28 100644
--- a/web_src/js/features/repo-issue-sidebar.md
+++ b/web_src/js/features/repo-issue-sidebar.md
@@ -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.
diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts
index 52878848e8c..45cd38d533d 100644
--- a/web_src/js/features/repo-issue-sidebar.ts
+++ b/web_src/js/features/repo-issue-sidebar.ts
@@ -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));
 }