mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Fix counting and filtering on the dashboard page for issues (#26657)
This PR has multiple parts, and I didn't split them because it's not easy to test them separately since they are all about the dashboard page for issues. 1. Support counting issues via indexer to fix #26361 2. Fix repo selection so it also fixes #26653 3. Keep keywords in filter links. The first two are regressions of #26012. After: https://github.com/go-gitea/gitea/assets/9418365/71dfea7e-d9e2-42b6-851a-cc081435c946 Thanks to @CaiCandong for helping with some tests.
This commit is contained in:
		@@ -130,6 +130,10 @@ type SearchRepoOptions struct {
 | 
				
			|||||||
	// True -> include just collaborative
 | 
						// True -> include just collaborative
 | 
				
			||||||
	// False -> include just non-collaborative
 | 
						// False -> include just non-collaborative
 | 
				
			||||||
	Collaborate util.OptionalBool
 | 
						Collaborate util.OptionalBool
 | 
				
			||||||
 | 
						// What type of unit the user can be collaborative in,
 | 
				
			||||||
 | 
						// it is ignored if Collaborate is False.
 | 
				
			||||||
 | 
						// TypeInvalid means any unit type.
 | 
				
			||||||
 | 
						UnitType unit.Type
 | 
				
			||||||
	// None -> include forks AND non-forks
 | 
						// None -> include forks AND non-forks
 | 
				
			||||||
	// True -> include just forks
 | 
						// True -> include just forks
 | 
				
			||||||
	// False -> include just non-forks
 | 
						// False -> include just non-forks
 | 
				
			||||||
@@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		if opts.Collaborate != util.OptionalBoolFalse {
 | 
							if opts.Collaborate != util.OptionalBoolFalse {
 | 
				
			||||||
			// A Collaboration is:
 | 
								// A Collaboration is:
 | 
				
			||||||
			collaborateCond := builder.And(
 | 
					
 | 
				
			||||||
 | 
								collaborateCond := builder.NewCond()
 | 
				
			||||||
			// 1. Repository we don't own
 | 
								// 1. Repository we don't own
 | 
				
			||||||
				builder.Neq{"owner_id": opts.OwnerID},
 | 
								collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
 | 
				
			||||||
			// 2. But we can see because of:
 | 
								// 2. But we can see because of:
 | 
				
			||||||
				builder.Or(
 | 
								{
 | 
				
			||||||
 | 
									userAccessCond := builder.NewCond()
 | 
				
			||||||
				// A. We have unit independent access
 | 
									// A. We have unit independent access
 | 
				
			||||||
					UserAccessRepoCond("`repository`.id", opts.OwnerID),
 | 
									userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
 | 
				
			||||||
				// B. We are in a team for
 | 
									// B. We are in a team for
 | 
				
			||||||
					UserOrgTeamRepoCond("`repository`.id", opts.OwnerID),
 | 
									if opts.UnitType == unit.TypeInvalid {
 | 
				
			||||||
 | 
										userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID))
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				// C. Public repositories in organizations that we are member of
 | 
									// C. Public repositories in organizations that we are member of
 | 
				
			||||||
					userOrgPublicRepoCondPrivate(opts.OwnerID),
 | 
									userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
 | 
				
			||||||
				),
 | 
									collaborateCond = collaborateCond.And(userAccessCond)
 | 
				
			||||||
			)
 | 
								}
 | 
				
			||||||
			if !opts.Private {
 | 
								if !opts.Private {
 | 
				
			||||||
				collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
 | 
									collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	db_model "code.gitea.io/gitea/models/db"
 | 
						db_model "code.gitea.io/gitea/models/db"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/container"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/graceful"
 | 
						"code.gitea.io/gitea/modules/graceful"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/indexer/issues/bleve"
 | 
						"code.gitea.io/gitea/modules/indexer/issues/bleve"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/indexer/issues/db"
 | 
						"code.gitea.io/gitea/modules/indexer/issues/db"
 | 
				
			||||||
@@ -277,7 +278,7 @@ func IsAvailable(ctx context.Context) bool {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SearchOptions indicates the options for searching issues
 | 
					// SearchOptions indicates the options for searching issues
 | 
				
			||||||
type SearchOptions internal.SearchOptions
 | 
					type SearchOptions = internal.SearchOptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	SortByCreatedDesc  = internal.SortByCreatedDesc
 | 
						SortByCreatedDesc  = internal.SortByCreatedDesc
 | 
				
			||||||
@@ -291,7 +292,6 @@ const (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SearchIssues search issues by options.
 | 
					// SearchIssues search issues by options.
 | 
				
			||||||
// It returns issue ids and a bool value indicates if the result is imprecise.
 | 
					 | 
				
			||||||
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
 | 
					func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
 | 
				
			||||||
	indexer := *globalIndexer.Load()
 | 
						indexer := *globalIndexer.Load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
 | 
				
			|||||||
		indexer = db.NewIndexer()
 | 
							indexer = db.NewIndexer()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts))
 | 
						result, err := indexer.Search(ctx, opts)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, 0, err
 | 
							return nil, 0, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return ret, result.Total, nil
 | 
						return ret, result.Total, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
 | 
				
			||||||
 | 
					func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) {
 | 
				
			||||||
 | 
						opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, total, err := SearchIssues(ctx, opts)
 | 
				
			||||||
 | 
						return total, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CountIssuesByRepo counts issues by options and group by repo id.
 | 
				
			||||||
 | 
					// It's not a complete implementation, since it requires the caller should provide the repo ids.
 | 
				
			||||||
 | 
					// That means opts.RepoIDs must be specified, and opts.AllPublic must be false.
 | 
				
			||||||
 | 
					// It's good enough for the current usage, and it can be improved if needed.
 | 
				
			||||||
 | 
					// TODO: use "group by" of the indexer engines to implement it.
 | 
				
			||||||
 | 
					func CountIssuesByRepo(ctx context.Context, opts *SearchOptions) (map[int64]int64, error) {
 | 
				
			||||||
 | 
						if len(opts.RepoIDs) == 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("opts.RepoIDs must be specified")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.AllPublic {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("opts.AllPublic must be false")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repoIDs := container.SetOf(opts.RepoIDs...).Values()
 | 
				
			||||||
 | 
						ret := make(map[int64]int64, len(repoIDs))
 | 
				
			||||||
 | 
						// TODO: it could be faster if do it in parallel for some indexer engines. Improve it if users report it's slow.
 | 
				
			||||||
 | 
						for _, repoID := range repoIDs {
 | 
				
			||||||
 | 
							count, err := CountIssues(ctx, opts.Copy(func(o *internal.SearchOptions) { o.RepoIDs = []int64{repoID} }))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ret[repoID] = count
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ret, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -109,6 +109,19 @@ type SearchOptions struct {
 | 
				
			|||||||
	SortBy SortBy // sort by field
 | 
						SortBy SortBy // sort by field
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Copy returns a copy of the options.
 | 
				
			||||||
 | 
					// Be careful, it's not a deep copy, so `SearchOptions.RepoIDs = {...}` is OK while `SearchOptions.RepoIDs[0] = ...` is not.
 | 
				
			||||||
 | 
					func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOptions {
 | 
				
			||||||
 | 
						if o == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						v := *o
 | 
				
			||||||
 | 
						for _, e := range edit {
 | 
				
			||||||
 | 
							e(&v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &v
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SortBy string
 | 
					type SortBy string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -453,16 +453,21 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
		Private:     true,
 | 
							Private:     true,
 | 
				
			||||||
		AllPublic:   false,
 | 
							AllPublic:   false,
 | 
				
			||||||
		AllLimited:  false,
 | 
							AllLimited:  false,
 | 
				
			||||||
 | 
							Collaborate: util.OptionalBoolNone,
 | 
				
			||||||
 | 
							UnitType:    unitType,
 | 
				
			||||||
 | 
							Archived:    util.OptionalBoolFalse,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if team != nil {
 | 
						if team != nil {
 | 
				
			||||||
		repoOpts.TeamID = team.ID
 | 
							repoOpts.TeamID = team.ID
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						accessibleRepos := container.Set[int64]{}
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ids, _, err := repo_model.SearchRepositoryIDs(repoOpts)
 | 
							ids, _, err := repo_model.SearchRepositoryIDs(repoOpts)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			ctx.ServerError("SearchRepositoryIDs", err)
 | 
								ctx.ServerError("SearchRepositoryIDs", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							accessibleRepos.AddMultiple(ids...)
 | 
				
			||||||
		opts.RepoIDs = ids
 | 
							opts.RepoIDs = ids
 | 
				
			||||||
		if len(opts.RepoIDs) == 0 {
 | 
							if len(opts.RepoIDs) == 0 {
 | 
				
			||||||
			// no repos found, don't let the indexer return all repos
 | 
								// no repos found, don't let the indexer return all repos
 | 
				
			||||||
@@ -489,41 +494,17 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
	keyword := strings.Trim(ctx.FormString("q"), " ")
 | 
						keyword := strings.Trim(ctx.FormString("q"), " ")
 | 
				
			||||||
	ctx.Data["Keyword"] = keyword
 | 
						ctx.Data["Keyword"] = keyword
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	accessibleRepos := container.Set[int64]{}
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		ids, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			ctx.ServerError("GetRepoIDsForIssuesOptions", err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		for _, id := range ids {
 | 
					 | 
				
			||||||
			accessibleRepos.Add(id)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Educated guess: Do or don't show closed issues.
 | 
						// Educated guess: Do or don't show closed issues.
 | 
				
			||||||
	isShowClosed := ctx.FormString("state") == "closed"
 | 
						isShowClosed := ctx.FormString("state") == "closed"
 | 
				
			||||||
	opts.IsClosed = util.OptionalBoolOf(isShowClosed)
 | 
						opts.IsClosed = util.OptionalBoolOf(isShowClosed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Filter repos and count issues in them. Count will be used later.
 | 
						// Filter repos and count issues in them. Count will be used later.
 | 
				
			||||||
	// USING NON-FINAL STATE OF opts FOR A QUERY.
 | 
						// USING NON-FINAL STATE OF opts FOR A QUERY.
 | 
				
			||||||
	var issueCountByRepo map[int64]int64
 | 
						issueCountByRepo, err := issue_indexer.CountIssuesByRepo(ctx, issue_indexer.ToSearchOptions(keyword, opts))
 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		issueIDs, err := issueIDsFromSearch(ctx, keyword, opts)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			ctx.ServerError("issueIDsFromSearch", err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if len(issueIDs) > 0 { // else, no issues found, just leave issueCountByRepo empty
 | 
					 | 
				
			||||||
			opts.IssueIDs = issueIDs
 | 
					 | 
				
			||||||
			issueCountByRepo, err = issues_model.CountIssuesByRepo(ctx, opts)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("CountIssuesByRepo", err)
 | 
							ctx.ServerError("CountIssuesByRepo", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
			opts.IssueIDs = nil // reset, the opts will be used later
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Make sure page number is at least 1. Will be posted to ctx.Data.
 | 
						// Make sure page number is at least 1. Will be posted to ctx.Data.
 | 
				
			||||||
	page := ctx.FormInt("page")
 | 
						page := ctx.FormInt("page")
 | 
				
			||||||
@@ -551,13 +532,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Parse ctx.FormString("repos") and remember matched repo IDs for later.
 | 
						// Parse ctx.FormString("repos") and remember matched repo IDs for later.
 | 
				
			||||||
	// Gets set when clicking filters on the issues overview page.
 | 
						// Gets set when clicking filters on the issues overview page.
 | 
				
			||||||
	repoIDs := getRepoIDs(ctx.FormString("repos"))
 | 
						selectedRepoIDs := getRepoIDs(ctx.FormString("repos"))
 | 
				
			||||||
	if len(repoIDs) > 0 {
 | 
					 | 
				
			||||||
	// Remove repo IDs that are not accessible to the user.
 | 
						// Remove repo IDs that are not accessible to the user.
 | 
				
			||||||
		repoIDs = util.SliceRemoveAllFunc(repoIDs, func(v int64) bool {
 | 
						selectedRepoIDs = util.SliceRemoveAllFunc(selectedRepoIDs, func(v int64) bool {
 | 
				
			||||||
		return !accessibleRepos.Contains(v)
 | 
							return !accessibleRepos.Contains(v)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
		opts.RepoIDs = repoIDs
 | 
						if len(selectedRepoIDs) > 0 {
 | 
				
			||||||
 | 
							opts.RepoIDs = selectedRepoIDs
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ------------------------------
 | 
						// ------------------------------
 | 
				
			||||||
@@ -568,7 +549,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
	// USING FINAL STATE OF opts FOR A QUERY.
 | 
						// USING FINAL STATE OF opts FOR A QUERY.
 | 
				
			||||||
	var issues issues_model.IssueList
 | 
						var issues issues_model.IssueList
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		issueIDs, err := issueIDsFromSearch(ctx, keyword, opts)
 | 
							issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			ctx.ServerError("issueIDsFromSearch", err)
 | 
								ctx.ServerError("issueIDsFromSearch", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -584,6 +565,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
	// Add repository pointers to Issues.
 | 
						// Add repository pointers to Issues.
 | 
				
			||||||
	// ----------------------------------
 | 
						// ----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove repositories that should not be shown,
 | 
				
			||||||
 | 
						// which are repositories that have no issues and are not selected by the user.
 | 
				
			||||||
 | 
						selectedReposMap := make(map[int64]struct{}, len(selectedRepoIDs))
 | 
				
			||||||
 | 
						for _, repoID := range selectedRepoIDs {
 | 
				
			||||||
 | 
							selectedReposMap[repoID] = struct{}{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for k, v := range issueCountByRepo {
 | 
				
			||||||
 | 
							if _, ok := selectedReposMap[k]; !ok && v == 0 {
 | 
				
			||||||
 | 
								delete(issueCountByRepo, k)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// showReposMap maps repository IDs to their Repository pointers.
 | 
						// showReposMap maps repository IDs to their Repository pointers.
 | 
				
			||||||
	showReposMap, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType)
 | 
						showReposMap, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -615,45 +608,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
	// -------------------------------
 | 
						// -------------------------------
 | 
				
			||||||
	// Fill stats to post to ctx.Data.
 | 
						// Fill stats to post to ctx.Data.
 | 
				
			||||||
	// -------------------------------
 | 
						// -------------------------------
 | 
				
			||||||
	var issueStats *issues_model.IssueStats
 | 
						issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts), ctx.Doer.ID)
 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		statsOpts := issues_model.IssuesOptions{
 | 
					 | 
				
			||||||
			RepoIDs:    repoIDs,
 | 
					 | 
				
			||||||
			User:       ctx.Doer,
 | 
					 | 
				
			||||||
			IsPull:     util.OptionalBoolOf(isPullList),
 | 
					 | 
				
			||||||
			IsClosed:   util.OptionalBoolOf(isShowClosed),
 | 
					 | 
				
			||||||
			IssueIDs:   nil,
 | 
					 | 
				
			||||||
			IsArchived: util.OptionalBoolFalse,
 | 
					 | 
				
			||||||
			LabelIDs:   opts.LabelIDs,
 | 
					 | 
				
			||||||
			Org:        org,
 | 
					 | 
				
			||||||
			Team:       team,
 | 
					 | 
				
			||||||
			RepoCond:   opts.RepoCond,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if keyword != "" {
 | 
					 | 
				
			||||||
			statsOpts.RepoIDs = opts.RepoIDs
 | 
					 | 
				
			||||||
			allIssueIDs, err := issueIDsFromSearch(ctx, keyword, &statsOpts)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
				ctx.ServerError("issueIDsFromSearch", err)
 | 
							ctx.ServerError("getUserIssueStats", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
			statsOpts.IssueIDs = allIssueIDs
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if keyword != "" && len(statsOpts.IssueIDs) == 0 {
 | 
					 | 
				
			||||||
			// So it did search with the keyword, but no issue found.
 | 
					 | 
				
			||||||
			// Just set issueStats to empty.
 | 
					 | 
				
			||||||
			issueStats = &issues_model.IssueStats{}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
 | 
					 | 
				
			||||||
			// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
 | 
					 | 
				
			||||||
			issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				ctx.ServerError("GetUserIssueStats", err)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Will be posted to ctx.Data.
 | 
						// Will be posted to ctx.Data.
 | 
				
			||||||
	var shownIssues int
 | 
						var shownIssues int
 | 
				
			||||||
@@ -722,7 +681,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
				
			|||||||
	ctx.Data["IssueStats"] = issueStats
 | 
						ctx.Data["IssueStats"] = issueStats
 | 
				
			||||||
	ctx.Data["ViewType"] = viewType
 | 
						ctx.Data["ViewType"] = viewType
 | 
				
			||||||
	ctx.Data["SortType"] = sortType
 | 
						ctx.Data["SortType"] = sortType
 | 
				
			||||||
	ctx.Data["RepoIDs"] = opts.RepoIDs
 | 
						ctx.Data["RepoIDs"] = selectedRepoIDs
 | 
				
			||||||
	ctx.Data["IsShowClosed"] = isShowClosed
 | 
						ctx.Data["IsShowClosed"] = isShowClosed
 | 
				
			||||||
	ctx.Data["SelectLabels"] = selectedLabels
 | 
						ctx.Data["SelectLabels"] = selectedLabels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -777,14 +736,6 @@ func getRepoIDs(reposQuery string) []int64 {
 | 
				
			|||||||
	return repoIDs
 | 
						return repoIDs
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
 | 
					 | 
				
			||||||
	ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("SearchIssues: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ids, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func loadRepoByIDs(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) {
 | 
					func loadRepoByIDs(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) {
 | 
				
			||||||
	totalRes := make(map[int64]*repo_model.Repository, len(issueCountByRepo))
 | 
						totalRes := make(map[int64]*repo_model.Repository, len(issueCountByRepo))
 | 
				
			||||||
	repoIDs := make([]int64, 0, 500)
 | 
						repoIDs := make([]int64, 0, 500)
 | 
				
			||||||
@@ -913,3 +864,71 @@ func UsernameSubRoute(ctx *context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions, doerID int64) (*issues_model.IssueStats, error) {
 | 
				
			||||||
 | 
						opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
 | 
				
			||||||
 | 
							o.AssigneeID = nil
 | 
				
			||||||
 | 
							o.PosterID = nil
 | 
				
			||||||
 | 
							o.MentionID = nil
 | 
				
			||||||
 | 
							o.ReviewRequestedID = nil
 | 
				
			||||||
 | 
							o.ReviewedID = nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							err error
 | 
				
			||||||
 | 
							ret = &issues_model.IssueStats{}
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							openClosedOpts := opts.Copy()
 | 
				
			||||||
 | 
							switch filterMode {
 | 
				
			||||||
 | 
							case issues_model.FilterModeAll, issues_model.FilterModeYourRepositories:
 | 
				
			||||||
 | 
							case issues_model.FilterModeAssign:
 | 
				
			||||||
 | 
								openClosedOpts.AssigneeID = &doerID
 | 
				
			||||||
 | 
							case issues_model.FilterModeCreate:
 | 
				
			||||||
 | 
								openClosedOpts.PosterID = &doerID
 | 
				
			||||||
 | 
							case issues_model.FilterModeMention:
 | 
				
			||||||
 | 
								openClosedOpts.MentionID = &doerID
 | 
				
			||||||
 | 
							case issues_model.FilterModeReviewRequested:
 | 
				
			||||||
 | 
								openClosedOpts.ReviewRequestedID = &doerID
 | 
				
			||||||
 | 
							case issues_model.FilterModeReviewed:
 | 
				
			||||||
 | 
								openClosedOpts.ReviewedID = &doerID
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							openClosedOpts.IsClosed = util.OptionalBoolFalse
 | 
				
			||||||
 | 
							ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							openClosedOpts.IsClosed = util.OptionalBoolTrue
 | 
				
			||||||
 | 
							ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID }))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID }))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID }))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID }))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID }))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,29 +5,29 @@
 | 
				
			|||||||
		<div class="ui stackable grid">
 | 
							<div class="ui stackable grid">
 | 
				
			||||||
			<div class="four wide column">
 | 
								<div class="four wide column">
 | 
				
			||||||
				<div class="ui secondary vertical filter menu gt-bg-transparent">
 | 
									<div class="ui secondary vertical filter menu gt-bg-transparent">
 | 
				
			||||||
					<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
 | 
										<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 | 
				
			||||||
						{{.locale.Tr "home.issues.in_your_repos"}}
 | 
											{{.locale.Tr "home.issues.in_your_repos"}}
 | 
				
			||||||
						<strong class="ui right">{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
 | 
											<strong class="ui right">{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
					<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
 | 
										<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 | 
				
			||||||
						{{.locale.Tr "repo.issues.filter_type.assigned_to_you"}}
 | 
											{{.locale.Tr "repo.issues.filter_type.assigned_to_you"}}
 | 
				
			||||||
						<strong class="ui right">{{CountFmt .IssueStats.AssignCount}}</strong>
 | 
											<strong class="ui right">{{CountFmt .IssueStats.AssignCount}}</strong>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
					<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
 | 
										<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 | 
				
			||||||
						{{.locale.Tr "repo.issues.filter_type.created_by_you"}}
 | 
											{{.locale.Tr "repo.issues.filter_type.created_by_you"}}
 | 
				
			||||||
						<strong class="ui right">{{CountFmt .IssueStats.CreateCount}}</strong>
 | 
											<strong class="ui right">{{CountFmt .IssueStats.CreateCount}}</strong>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
					{{if .PageIsPulls}}
 | 
										{{if .PageIsPulls}}
 | 
				
			||||||
						<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
 | 
											<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 | 
				
			||||||
							{{.locale.Tr "repo.issues.filter_type.review_requested"}}
 | 
												{{.locale.Tr "repo.issues.filter_type.review_requested"}}
 | 
				
			||||||
							<strong class="ui right">{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
 | 
												<strong class="ui right">{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
						<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
 | 
											<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 | 
				
			||||||
							{{.locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
 | 
												{{.locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
 | 
				
			||||||
							<strong class="ui right">{{CountFmt .IssueStats.ReviewedCount}}</strong>
 | 
												<strong class="ui right">{{CountFmt .IssueStats.ReviewedCount}}</strong>
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
					<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
 | 
										<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 | 
				
			||||||
						{{.locale.Tr "repo.issues.filter_type.mentioning_you"}}
 | 
											{{.locale.Tr "repo.issues.filter_type.mentioning_you"}}
 | 
				
			||||||
						<strong class="ui right">{{CountFmt .IssueStats.MentionCount}}</strong>
 | 
											<strong class="ui right">{{CountFmt .IssueStats.MentionCount}}</strong>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user