@@ -504,6 +504,10 @@ ul li:not(:last-child) {
.repo-list-link {
min-width: 0; /* for text truncation */
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 0.5rem;
}
.repo-list-link .svg {
From 75689b8973ccf149b39f9b883fe312ca993ba6bc Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 22 Aug 2023 18:20:09 +0900
Subject: [PATCH 2/6] Improve repo sub menu (#26531)
Before:

After:

Icons are not in the middle of the line.
---
templates/repo/sub_menu.tmpl | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 27b707d33284a..da5f37fa05765 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -4,20 +4,20 @@
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
{{if $.Permission.CanRead $.UnitTypeCode}}
{{end}}
{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
- {{svg "octicon-database"}} {{.locale.PrettyNumber (index $fileSizeFields 0)}} {{index $fileSizeFields 1}}
+ {{svg "octicon-database"}} {{.locale.PrettyNumber (index $fileSizeFields 0)}} {{index $fileSizeFields 1}}
{{end}}
From 8f2e2878e576e824bce8f477f669ce78b158ede8 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Tue, 22 Aug 2023 18:19:15 +0800
Subject: [PATCH 3/6] Use line-height: normal by default (#26635)
Fix #26537 again because 1.15 is too small for some fonts.
---
web_src/css/base.css | 6 +-----
web_src/css/modules/normalize.css | 2 +-
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 78609ab0de7db..d9c57932fc2ec 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -10,7 +10,7 @@
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* line-height: use the default value as "modules/normalize.css" */
- --line-height-default: 1.15;
+ --line-height-default: normal;
/* backgrounds */
--checkbox-mask-checked: url('data:image/svg+xml;utf8,');
--checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,');
@@ -521,8 +521,6 @@ a.label,
color: var(--color-text);
user-select: auto;
line-height: var(--line-height-default); /* fomantic uses "1" which causes overflow problems because "1" doesn't consider the descent part */
- padding-top: 11px; /* counteract line-height change */
- padding-bottom: 11px; /* counteract line-height change */
}
.ui.menu .item > .svg {
@@ -669,8 +667,6 @@ a.label,
.ui.secondary.menu .item {
margin-left: 0;
margin-right: 0;
- padding-top: 10px; /* counteract line-height change */
- padding-bottom: 10px; /* counteract line-height change */
}
.ui.secondary.menu .dropdown.item:hover,
diff --git a/web_src/css/modules/normalize.css b/web_src/css/modules/normalize.css
index 809ed3d7ccfff..63fb04a57789a 100644
--- a/web_src/css/modules/normalize.css
+++ b/web_src/css/modules/normalize.css
@@ -25,7 +25,7 @@ Use a better box model (opinionated).
}
html {
- line-height: 1.15; /* 1. Correct the line height in all browsers. */
+ line-height: normal; /* 1. (not following the "modern-normalize") Do not change the browser's default line-height, the default value is font-dependent and roughly 1.2 */
-webkit-text-size-adjust: 100%; /* 2. Prevent adjustments of font size after orientation changes in iOS. */
}
From 3a67997f98f070fed9e0b9426891cbceb28ebf17 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 23 Aug 2023 03:29:49 +0200
Subject: [PATCH 4/6] [Refactor] getIssueStatsChunk to move inner function into
own one (#26671)
move inner **countSession** of **getIssueStatsChunk** into it's own
function for reuse
---
*Sponsored by Kithara Software GmbH*
---
models/issues/issue_stats.go | 97 ++++++++++++++++++------------------
1 file changed, 49 insertions(+), 48 deletions(-)
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 6c249c2244160..1654e6ce756ff 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -116,68 +116,69 @@ func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) {
func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) {
stats := &IssueStats{}
- countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
- sess := db.GetEngine(db.DefaultContext).
- Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
- if len(opts.RepoIDs) > 1 {
- sess.In("issue.repo_id", opts.RepoIDs)
- } else if len(opts.RepoIDs) == 1 {
- sess.And("issue.repo_id = ?", opts.RepoIDs[0])
- }
+ sess := db.GetEngine(db.DefaultContext).
+ Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
- if len(issueIDs) > 0 {
- sess.In("issue.id", issueIDs)
- }
+ var err error
+ stats.OpenCount, err = applyIssuesOptions(sess, opts, issueIDs).
+ And("issue.is_closed = ?", false).
+ Count(new(Issue))
+ if err != nil {
+ return stats, err
+ }
+ stats.ClosedCount, err = applyIssuesOptions(sess, opts, issueIDs).
+ And("issue.is_closed = ?", true).
+ Count(new(Issue))
+ return stats, err
+}
- applyLabelsCondition(sess, opts)
+func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int64) *xorm.Session {
+ if len(opts.RepoIDs) > 1 {
+ sess.In("issue.repo_id", opts.RepoIDs)
+ } else if len(opts.RepoIDs) == 1 {
+ sess.And("issue.repo_id = ?", opts.RepoIDs[0])
+ }
- applyMilestoneCondition(sess, opts)
+ if len(issueIDs) > 0 {
+ sess.In("issue.id", issueIDs)
+ }
- applyProjectCondition(sess, opts)
+ applyLabelsCondition(sess, opts)
- if opts.AssigneeID > 0 {
- applyAssigneeCondition(sess, opts.AssigneeID)
- } else if opts.AssigneeID == db.NoConditionID {
- sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
- }
+ applyMilestoneCondition(sess, opts)
- if opts.PosterID > 0 {
- applyPosterCondition(sess, opts.PosterID)
- }
+ applyProjectCondition(sess, opts)
- if opts.MentionedID > 0 {
- applyMentionedCondition(sess, opts.MentionedID)
- }
+ if opts.AssigneeID > 0 {
+ applyAssigneeCondition(sess, opts.AssigneeID)
+ } else if opts.AssigneeID == db.NoConditionID {
+ sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
+ }
- if opts.ReviewRequestedID > 0 {
- applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
- }
+ if opts.PosterID > 0 {
+ applyPosterCondition(sess, opts.PosterID)
+ }
- if opts.ReviewedID > 0 {
- applyReviewedCondition(sess, opts.ReviewedID)
- }
+ if opts.MentionedID > 0 {
+ applyMentionedCondition(sess, opts.MentionedID)
+ }
- switch opts.IsPull {
- case util.OptionalBoolTrue:
- sess.And("issue.is_pull=?", true)
- case util.OptionalBoolFalse:
- sess.And("issue.is_pull=?", false)
- }
+ if opts.ReviewRequestedID > 0 {
+ applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
+ }
- return sess
+ if opts.ReviewedID > 0 {
+ applyReviewedCondition(sess, opts.ReviewedID)
}
- var err error
- stats.OpenCount, err = countSession(opts, issueIDs).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return stats, err
+ switch opts.IsPull {
+ case util.OptionalBoolTrue:
+ sess.And("issue.is_pull=?", true)
+ case util.OptionalBoolFalse:
+ sess.And("issue.is_pull=?", false)
}
- stats.ClosedCount, err = countSession(opts, issueIDs).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- return stats, err
+
+ return sess
}
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
From 3b91b2d6b12b9c9c18406f484775925bbd557618 Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Wed, 23 Aug 2023 09:56:11 +0800
Subject: [PATCH 5/6] add mfa doc (#26654)
copy and modified from #14572
> Whilst debating enforcing MFA within our team, I realised there isn't
a lot of context to the side effects of enabling it. Most of us use Git
over HTTP and would need to add a token.
I plan to add another PR that adds a sentence to the UI about needing to
generate a token when enabling MFA if HTTP is to be used.
---------
Co-authored-by: techknowlogick
Co-authored-by: silverwind
---
.../multi-factor-authentication.en-us.md | 35 +++++++++++++++++++
1 file changed, 35 insertions(+)
create mode 100644 docs/content/usage/multi-factor-authentication.en-us.md
diff --git a/docs/content/usage/multi-factor-authentication.en-us.md b/docs/content/usage/multi-factor-authentication.en-us.md
new file mode 100644
index 0000000000000..16b57b7bdca74
--- /dev/null
+++ b/docs/content/usage/multi-factor-authentication.en-us.md
@@ -0,0 +1,35 @@
+---
+date: "2023-08-22T14:21:00+08:00"
+title: "Usage: Multi-factor Authentication (MFA)"
+slug: "multi-factor-authentication"
+weight: 15
+toc: false
+draft: false
+menu:
+ sidebar:
+ parent: "usage"
+ name: "Multi-factor Authentication (MFA)"
+ weight: 15
+ identifier: "multi-factor-authentication"
+---
+
+# Multi-factor Authentication (MFA)
+
+Multi-factor Authentication (also referred to as MFA or 2FA) enhances security by requiring a time-sensitive set of credentials in addition to a password.
+If a password were later to be compromised, logging into Gitea will not be possible without the additional credentials and the account would remain secure.
+Gitea supports both TOTP (Time-based One-Time Password) tokens and FIDO-based hardware keys using the Webauthn API.
+
+MFA can be configured within the "Security" tab of the user settings page.
+
+## MFA Considerations
+
+Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI.
+This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled.
+If SSH is not an option for Git operations, an access token can be generated within the "Applications" tab of the user settings page.
+This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP.
+
+> **Warning** - By its very nature, an access token sidesteps the security benefits of MFA.
+> It must be kept secure and should only be used as a last resort.
+
+The Gitea API supports providing the relevant TOTP password in the `X-Gitea-OTP` header, as described in [API Usage](development/api-usage.md).
+This should be used instead of an access token where possible.
From 5db21ce7e10ba78ede8841bea9db7a63adbececb Mon Sep 17 00:00:00 2001
From: Jason Song
Date: Wed, 23 Aug 2023 10:29:17 +0800
Subject: [PATCH 6/6] 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.
---
models/repo/repo_list.go | 36 +++--
modules/indexer/issues/indexer.go | 41 ++++-
modules/indexer/issues/internal/model.go | 13 ++
routers/web/user/home.go | 195 +++++++++++++----------
templates/user/dashboard/issues.tmpl | 12 +-
5 files changed, 187 insertions(+), 110 deletions(-)
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index b8427bec4eb45..a0485ed8d417d 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -130,6 +130,10 @@ type SearchRepoOptions struct {
// True -> include just collaborative
// False -> include just non-collaborative
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
// True -> include just forks
// False -> include just non-forks
@@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
if opts.Collaborate != util.OptionalBoolFalse {
// A Collaboration is:
- collaborateCond := builder.And(
- // 1. Repository we don't own
- builder.Neq{"owner_id": opts.OwnerID},
- // 2. But we can see because of:
- builder.Or(
- // A. We have unit independent access
- UserAccessRepoCond("`repository`.id", opts.OwnerID),
- // B. We are in a team for
- UserOrgTeamRepoCond("`repository`.id", opts.OwnerID),
- // C. Public repositories in organizations that we are member of
- userOrgPublicRepoCondPrivate(opts.OwnerID),
- ),
- )
+
+ collaborateCond := builder.NewCond()
+ // 1. Repository we don't own
+ collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
+ // 2. But we can see because of:
+ {
+ userAccessCond := builder.NewCond()
+ // A. We have unit independent access
+ userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
+ // B. We are in a team for
+ 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
+ userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
+ collaborateCond = collaborateCond.And(userAccessCond)
+ }
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))
}
diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go
index 6619949104f49..020659c82b541 100644
--- a/modules/indexer/issues/indexer.go
+++ b/modules/indexer/issues/indexer.go
@@ -13,6 +13,7 @@ import (
db_model "code.gitea.io/gitea/models/db"
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/indexer/issues/bleve"
"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
-type SearchOptions internal.SearchOptions
+type SearchOptions = internal.SearchOptions
const (
SortByCreatedDesc = internal.SortByCreatedDesc
@@ -291,7 +292,6 @@ const (
)
// 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) {
indexer := *globalIndexer.Load()
@@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
indexer = db.NewIndexer()
}
- result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts))
+ result, err := indexer.Search(ctx, opts)
if err != nil {
return nil, 0, err
}
@@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
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
+}
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index 2de1e0e2bf20b..031745dd2fcc1 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -109,6 +109,19 @@ type SearchOptions struct {
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
const (
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 8c1447f707863..d1a4877e6d4b6 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -448,21 +448,26 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// - Team org's owns the repository.
// - Team has read permission to repository.
repoOpts := &repo_model.SearchRepoOptions{
- Actor: ctx.Doer,
- OwnerID: ctx.Doer.ID,
- Private: true,
- AllPublic: false,
- AllLimited: false,
+ Actor: ctx.Doer,
+ OwnerID: ctx.Doer.ID,
+ Private: true,
+ AllPublic: false,
+ AllLimited: false,
+ Collaborate: util.OptionalBoolNone,
+ UnitType: unitType,
+ Archived: util.OptionalBoolFalse,
}
if team != nil {
repoOpts.TeamID = team.ID
}
+ accessibleRepos := container.Set[int64]{}
{
ids, _, err := repo_model.SearchRepositoryIDs(repoOpts)
if err != nil {
ctx.ServerError("SearchRepositoryIDs", err)
return
}
+ accessibleRepos.AddMultiple(ids...)
opts.RepoIDs = ids
if len(opts.RepoIDs) == 0 {
// no repos found, don't let the indexer return all repos
@@ -489,40 +494,16 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
keyword := strings.Trim(ctx.FormString("q"), " ")
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.
isShowClosed := ctx.FormString("state") == "closed"
opts.IsClosed = util.OptionalBoolOf(isShowClosed)
// Filter repos and count issues in them. Count will be used later.
// USING NON-FINAL STATE OF opts FOR A QUERY.
- var issueCountByRepo map[int64]int64
- {
- 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 {
- ctx.ServerError("CountIssuesByRepo", err)
- return
- }
- opts.IssueIDs = nil // reset, the opts will be used later
- }
+ issueCountByRepo, err := issue_indexer.CountIssuesByRepo(ctx, issue_indexer.ToSearchOptions(keyword, opts))
+ if err != nil {
+ ctx.ServerError("CountIssuesByRepo", err)
+ return
}
// Make sure page number is at least 1. Will be posted to ctx.Data.
@@ -551,13 +532,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
// Gets set when clicking filters on the issues overview page.
- repoIDs := getRepoIDs(ctx.FormString("repos"))
- if len(repoIDs) > 0 {
- // Remove repo IDs that are not accessible to the user.
- repoIDs = util.SliceRemoveAllFunc(repoIDs, func(v int64) bool {
- return !accessibleRepos.Contains(v)
- })
- opts.RepoIDs = repoIDs
+ selectedRepoIDs := getRepoIDs(ctx.FormString("repos"))
+ // Remove repo IDs that are not accessible to the user.
+ selectedRepoIDs = util.SliceRemoveAllFunc(selectedRepoIDs, func(v int64) bool {
+ return !accessibleRepos.Contains(v)
+ })
+ 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.
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 {
ctx.ServerError("issueIDsFromSearch", err)
return
@@ -584,6 +565,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// 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, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType)
if err != nil {
@@ -615,44 +608,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
- var issueStats *issues_model.IssueStats
- {
- 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 {
- ctx.ServerError("issueIDsFromSearch", err)
- 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
- }
- }
+ issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts), ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("getUserIssueStats", err)
+ return
}
// Will be posted to ctx.Data.
@@ -722,7 +681,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["IssueStats"] = issueStats
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
- ctx.Data["RepoIDs"] = opts.RepoIDs
+ ctx.Data["RepoIDs"] = selectedRepoIDs
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["SelectLabels"] = selectedLabels
@@ -777,14 +736,6 @@ func getRepoIDs(reposQuery string) []int64 {
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) {
totalRes := make(map[int64]*repo_model.Repository, len(issueCountByRepo))
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
+}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 8d6cc67afe17e..a89098c6ab31b 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -5,29 +5,29 @@