Load mentionValues asynchronously (#36739)

Eliminate a few database queries on all issue and pull request pages by
moving mention autocomplete data to async JSON endpoints fetched
on-demand when the user types `@`.

See https://github.com/go-gitea/gitea/pull/36739#issuecomment-3963184858
for the full table of affected pages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-03-07 21:37:37 +01:00
committed by GitHub
parent f250138f57
commit 130e34994f
32 changed files with 363 additions and 161 deletions
-45
View File
@@ -14,7 +14,6 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/renderhelper"
@@ -649,47 +648,3 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
}
return attachHTML
}
// handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data
func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) {
// TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees"
// at the moment it is used on the issue list page, for the markdown editor mention
ctx.Data["Assignees"] = assignees
if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
return
}
var isAdmin bool
var err error
var teams []*organization.Team
org := organization.OrgFromUser(ctx.Repo.Owner)
// Admin has super access.
if ctx.Doer.IsAdmin {
isAdmin = true
} else {
isAdmin, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("IsOwnedBy", err)
return
}
}
if isAdmin {
teams, err = org.LoadTeams(ctx)
if err != nil {
ctx.ServerError("LoadTeams", err)
return
}
} else {
teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserTeams", err)
return
}
}
ctx.Data["MentionableTeams"] = teams
ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
}
+1 -4
View File
@@ -662,10 +662,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
ctx.ServerError("GetRepoAssignees", err)
return
}
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
if ctx.Written() {
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
+1 -2
View File
@@ -155,8 +155,7 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
}
d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
}
// FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees)
ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees
}
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
shared_mention "code.gitea.io/gitea/routers/web/shared/mention"
"code.gitea.io/gitea/services/context"
)
// GetMentionsInRepo returns JSON data for mention autocomplete (assignees, participants, mentionable teams).
func GetMentionsInRepo(ctx *context.Context) {
c := shared_mention.NewCollector()
// Get participants if issue_index is provided
if issueIndex := ctx.FormInt64("issue_index"); issueIndex > 0 {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetIssueByIndex", err)
return
}
if issue != nil {
userIDs, err := issue.GetParticipantIDsByIssue(ctx)
if err != nil {
ctx.ServerError("GetParticipantIDsByIssue", err)
return
}
users, err := user_model.GetUsersByIDs(ctx, userIDs)
if err != nil {
ctx.ServerError("GetUsersByIDs", err)
return
}
c.AddUsers(ctx, users)
}
}
// Get repo assignees
assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetRepoAssignees", err)
return
}
c.AddUsers(ctx, assignees)
// Get mentionable teams for org repos
if err := c.AddMentionableTeams(ctx, ctx.Doer, ctx.Repo.Owner); err != nil {
ctx.ServerError("AddMentionableTeams", err)
return
}
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(c.Result))
}
+1 -4
View File
@@ -913,10 +913,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
ctx.ServerError("GetRepoAssignees", err)
return
}
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
if ctx.Written() {
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
if err != nil && !issues_model.IsErrReviewNotExist(err) {