diff options
-rw-r--r-- | models/issue.go | 34 | ||||
-rw-r--r-- | models/issue_test.go | 44 | ||||
-rw-r--r-- | routers/user/home.go | 116 | ||||
-rw-r--r-- | templates/user/dashboard/issues.tmpl | 74 |
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> |