summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/issue.go34
-rw-r--r--models/issue_test.go44
-rw-r--r--routers/user/home.go116
-rw-r--r--templates/user/dashboard/issues.tmpl74
4 files changed, 214 insertions, 54 deletions
diff --git a/models/issue.go b/models/issue.go
index 340a431ad1..03af32700d 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -76,6 +76,7 @@ var (
const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)`
const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)`
const issueMaxDupIndexAttempts = 3
+const maxIssueIDs = 950
func init() {
issueTasksPat = regexp.MustCompile(issueTasksRegexpStr)
@@ -1098,6 +1099,9 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
}
if len(opts.IssueIDs) > 0 {
+ if len(opts.IssueIDs) > maxIssueIDs {
+ opts.IssueIDs = opts.IssueIDs[:maxIssueIDs]
+ }
sess.In("issue.id", opts.IssueIDs)
}
@@ -1176,6 +1180,26 @@ func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) {
return countMap, nil
}
+// GetRepoIDsForIssuesOptions find all repo ids for the given options
+func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *User) ([]int64, error) {
+ repoIDs := make([]int64, 0, 5)
+ sess := x.NewSession()
+ defer sess.Close()
+
+ opts.setupSession(sess)
+
+ accessCond := accessibleRepositoryCondition(user)
+ if err := sess.Where(accessCond).
+ Join("INNER", "repository", "`issue`.repo_id = `repository`.id").
+ Distinct("issue.repo_id").
+ Table("issue").
+ Find(&repoIDs); err != nil {
+ return nil, err
+ }
+
+ return repoIDs, nil
+}
+
// Issues returns a list of issues by given conditions.
func Issues(opts *IssuesOptions) ([]*Issue, error) {
sess := x.NewSession()
@@ -1313,6 +1337,9 @@ func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats,
Where("issue.repo_id = ?", opts.RepoID)
if len(opts.IssueIDs) > 0 {
+ if len(opts.IssueIDs) > maxIssueIDs {
+ opts.IssueIDs = opts.IssueIDs[:maxIssueIDs]
+ }
sess.In("issue.id", opts.IssueIDs)
}
@@ -1382,6 +1409,7 @@ type UserIssueStatsOptions struct {
FilterMode int
IsPull bool
IsClosed bool
+ IssueIDs []int64
}
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
@@ -1394,6 +1422,12 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
if len(opts.RepoIDs) > 0 {
cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs))
}
+ if len(opts.IssueIDs) > 0 {
+ if len(opts.IssueIDs) > maxIssueIDs {
+ opts.IssueIDs = opts.IssueIDs[:maxIssueIDs]
+ }
+ cond = cond.And(builder.In("issue.id", opts.IssueIDs))
+ }
switch opts.FilterMode {
case FilterModeAll:
diff --git a/models/issue_test.go b/models/issue_test.go
index 7ba9a396b2..e1995fc5d2 100644
--- a/models/issue_test.go
+++ b/models/issue_test.go
@@ -253,6 +253,20 @@ func TestGetUserIssueStats(t *testing.T) {
ClosedCount: 0,
},
},
+ {
+ UserIssueStatsOptions{
+ UserID: 1,
+ FilterMode: FilterModeCreate,
+ IssueIDs: []int64{1},
+ },
+ IssueStats{
+ YourRepositoriesCount: 0,
+ AssignCount: 1,
+ CreateCount: 1,
+ OpenCount: 1,
+ ClosedCount: 0,
+ },
+ },
} {
stats, err := GetUserIssueStats(test.Opts)
if !assert.NoError(t, err) {
@@ -294,6 +308,36 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
assert.EqualValues(t, []int64{1}, ids)
}
+func TestGetRepoIDsForIssuesOptions(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
+ for _, test := range []struct {
+ Opts IssuesOptions
+ ExpectedRepoIDs []int64
+ }{
+ {
+ IssuesOptions{
+ AssigneeID: 2,
+ },
+ []int64{3},
+ },
+ {
+ IssuesOptions{
+ RepoIDs: []int64{1, 2},
+ },
+ []int64{1, 2},
+ },
+ } {
+ repoIDs, err := GetRepoIDsForIssuesOptions(&test.Opts, user)
+ assert.NoError(t, err)
+ if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) {
+ for i, repoID := range repoIDs {
+ assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID)
+ }
+ }
+ }
+}
+
func testInsertIssue(t *testing.T, title, content string) {
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
diff --git a/routers/user/home.go b/routers/user/home.go
index 6b71c51de3..762fcca156 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
@@ -449,7 +450,6 @@ func Issues(ctx *context.Context) {
}
opts := &models.IssuesOptions{
- IsClosed: util.OptionalBoolOf(isShowClosed),
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
}
@@ -465,10 +465,39 @@ func Issues(ctx *context.Context) {
opts.MentionedID = ctxUser.ID
}
- counts, err := models.CountIssuesByRepo(opts)
- if err != nil {
- ctx.ServerError("CountIssuesByRepo", err)
- return
+ var forceEmpty bool
+ var issueIDsFromSearch []int64
+ var keyword = strings.Trim(ctx.Query("q"), " ")
+
+ if len(keyword) > 0 {
+ searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
+ if err != nil {
+ ctx.ServerError("GetRepoIDsForIssuesOptions", err)
+ return
+ }
+ issueIDsFromSearch, err = issue_indexer.SearchIssuesByKeyword(searchRepoIDs, keyword)
+ if err != nil {
+ ctx.ServerError("SearchIssuesByKeyword", err)
+ return
+ }
+ if len(issueIDsFromSearch) > 0 {
+ opts.IssueIDs = issueIDsFromSearch
+ } else {
+ forceEmpty = true
+ }
+ }
+
+ ctx.Data["Keyword"] = keyword
+
+ opts.IsClosed = util.OptionalBoolOf(isShowClosed)
+
+ var counts map[int64]int64
+ if !forceEmpty {
+ counts, err = models.CountIssuesByRepo(opts)
+ if err != nil {
+ ctx.ServerError("CountIssuesByRepo", err)
+ return
+ }
}
opts.Page = page
@@ -488,10 +517,15 @@ func Issues(ctx *context.Context) {
opts.RepoIDs = repoIDs
}
- issues, err := models.Issues(opts)
- if err != nil {
- ctx.ServerError("Issues", err)
- return
+ var issues []*models.Issue
+ if !forceEmpty {
+ issues, err = models.Issues(opts)
+ if err != nil {
+ ctx.ServerError("Issues", err)
+ return
+ }
+ } else {
+ issues = []*models.Issue{}
}
showReposMap := make(map[int64]*models.Repository, len(counts))
@@ -538,7 +572,7 @@ func Issues(ctx *context.Context) {
}
}
- issueStatsOpts := models.UserIssueStatsOptions{
+ userIssueStatsOpts := models.UserIssueStatsOptions{
UserID: ctxUser.ID,
UserRepoIDs: userRepoIDs,
FilterMode: filterMode,
@@ -546,33 +580,61 @@ func Issues(ctx *context.Context) {
IsClosed: isShowClosed,
}
if len(repoIDs) > 0 {
- issueStatsOpts.UserRepoIDs = repoIDs
+ userIssueStatsOpts.UserRepoIDs = repoIDs
}
- issueStats, err := models.GetUserIssueStats(issueStatsOpts)
+ userIssueStats, err := models.GetUserIssueStats(userIssueStatsOpts)
if err != nil {
- ctx.ServerError("GetUserIssueStats", err)
+ ctx.ServerError("GetUserIssueStats User", err)
return
}
- allIssueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{
- UserID: ctxUser.ID,
- UserRepoIDs: userRepoIDs,
- FilterMode: filterMode,
- IsPull: isPullList,
- IsClosed: isShowClosed,
- })
- if err != nil {
- ctx.ServerError("GetUserIssueStats All", err)
- return
+ var shownIssueStats *models.IssueStats
+ if !forceEmpty {
+ statsOpts := models.UserIssueStatsOptions{
+ UserID: ctxUser.ID,
+ UserRepoIDs: userRepoIDs,
+ FilterMode: filterMode,
+ IsPull: isPullList,
+ IsClosed: isShowClosed,
+ IssueIDs: issueIDsFromSearch,
+ }
+ if len(repoIDs) > 0 {
+ statsOpts.RepoIDs = repoIDs
+ }
+ shownIssueStats, err = models.GetUserIssueStats(statsOpts)
+ if err != nil {
+ ctx.ServerError("GetUserIssueStats Shown", err)
+ return
+ }
+ } else {
+ shownIssueStats = &models.IssueStats{}
+ }
+
+ var allIssueStats *models.IssueStats
+ if !forceEmpty {
+ allIssueStats, err = models.GetUserIssueStats(models.UserIssueStatsOptions{
+ UserID: ctxUser.ID,
+ UserRepoIDs: userRepoIDs,
+ FilterMode: filterMode,
+ IsPull: isPullList,
+ IsClosed: isShowClosed,
+ IssueIDs: issueIDsFromSearch,
+ })
+ if err != nil {
+ ctx.ServerError("GetUserIssueStats All", err)
+ return
+ }
+ } else {
+ allIssueStats = &models.IssueStats{}
}
var shownIssues int
var totalIssues int
if !isShowClosed {
- shownIssues = int(issueStats.OpenCount)
+ shownIssues = int(shownIssueStats.OpenCount)
totalIssues = int(allIssueStats.OpenCount)
} else {
- shownIssues = int(issueStats.ClosedCount)
+ shownIssues = int(shownIssueStats.ClosedCount)
totalIssues = int(allIssueStats.ClosedCount)
}
@@ -580,7 +642,8 @@ func Issues(ctx *context.Context) {
ctx.Data["CommitStatus"] = commitStatus
ctx.Data["Repos"] = showRepos
ctx.Data["Counts"] = counts
- ctx.Data["IssueStats"] = issueStats
+ ctx.Data["IssueStats"] = userIssueStats
+ ctx.Data["ShownIssueStats"] = shownIssueStats
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
ctx.Data["RepoIDs"] = repoIDs
@@ -599,6 +662,7 @@ func Issues(ctx *context.Context) {
ctx.Data["ReposParam"] = string(reposParam)
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
+ pager.AddParam(ctx, "q", "Keyword")
pager.AddParam(ctx, "type", "ViewType")
pager.AddParam(ctx, "repos", "ReposParam")
pager.AddParam(ctx, "sort", "SortType")
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index dfb94560e5..348af46857 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -24,7 +24,7 @@
</a>
{{end}}
<div class="ui divider"></div>
- <a class="{{if not $.RepoIDs}}ui basic blue button{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}">
+ <a class="{{if not $.RepoIDs}}ui basic blue button{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}">
<span class="text truncate">All</span>
<div class="ui {{if $.IsShowClosed}}red{{else}}green{{end}} label">{{.TotalIssueCount}}</div>
</a>
@@ -43,7 +43,7 @@
{{$Repo.ID}}%2C
{{end}}
{{end}}
- ]&sort={{$.SortType}}&state={{$.State}}" title="{{.FullName}}">
+ ]&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}" title="{{.FullName}}">
<span class="text truncate">{{$Repo.FullName}}</span>
<div class="ui {{if $.IsShowClosed}}red{{else}}green{{end}} label">{{index $.Counts $Repo.ID}}</div>
</a>
@@ -52,32 +52,50 @@
</div>
</div>
<div class="twelve wide column content">
- <div class="ui tiny basic status buttons">
- <a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open">
- {{svg "octicon-issue-opened" 16}}
- {{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}}
- </a>
- <a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed">
- {{svg "octicon-issue-closed" 16}}
- {{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}}
- </a>
- </div>
- <div class="ui right floated secondary filter menu">
- <!-- Sort -->
- <div class="ui dropdown type jump item">
- <span class="text">
- {{.i18n.Tr "repo.issues.filter_sort"}}
- <i class="dropdown icon"></i>
- </span>
- <div class="menu">
- <a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=latest&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a>
- <a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=oldest&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a>
- <a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=recentupdate&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a>
- <a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastupdate&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a>
- <a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomment&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a>
- <a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomment&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a>
- <a class="{{if eq .SortType "nearduedate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=nearduedate&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.nearduedate"}}</a>
- <a class="{{if eq .SortType "farduedate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=farduedate&state={{$.State}}">{{.i18n.Tr "repo.issues.filter_sort.farduedate"}}</a>
+ <div class="ui three column stackable grid">
+ <div class="column">
+ <div class="ui tiny basic status buttons">
+ <a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
+ {{svg "octicon-issue-opened" 16}}
+ {{.i18n.Tr "repo.issues.open_tab" .ShownIssueStats.OpenCount}}
+ </a>
+ <a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
+ {{svg "octicon-issue-closed" 16}}
+ {{.i18n.Tr "repo.issues.close_tab" .ShownIssueStats.ClosedCount}}
+ </a>
+ </div>
+ </div>
+ <div class="column center aligned">
+ <form class="ui form ignore-dirty">
+ <div class="ui fluid action input">
+ <input type="hidden" name="type" value="{{$.ViewType}}"/>
+ <input type="hidden" name="repos" value="[{{range $.RepoIDs}}{{.}}%2C{{end}}]"/>
+ <input type="hidden" name="sort" value="{{$.SortType}}"/>
+ <input type="hidden" name="state" value="{{$.State}}"/>
+ <div class="ui search action input">
+ <input name="q" value="{{$.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
+ </div>
+ <button class="ui blue button" type="submit">{{.i18n.Tr "explore.search"}}</button>
+ </div>
+ </form>
+ </div>
+ <div class="column right aligned">
+ <!-- Sort -->
+ <div class="ui dropdown type jump item">
+ <span class="text">
+ {{.i18n.Tr "repo.issues.filter_sort"}}
+ <i class="dropdown icon"></i>
+ </span>
+ <div class="menu">
+ <a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a>
+ <a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a>
+ <a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+ <a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+ <a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+ <a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+ <a class="{{if eq .SortType "nearduedate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+ <a class="{{if eq .SortType "farduedate"}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.farduedate"}}</a>
+ </div>
</div>
</div>
</div>