mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-06-11 15:40:02 -04:00
enh(issue-search): support filtering by project in user/org listing (#12596)
Support filtering `/issue` & `/pulls` (and corresponding org paths) by a project ID. Closes #12559 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12596 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
parent
db5b475416
commit
4ecb25a549
6 changed files with 129 additions and 10 deletions
|
|
@ -78,6 +78,6 @@
|
|||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 1
|
||||
type: 3
|
||||
created_unix: 1688973000
|
||||
updated_unix: 1688973000
|
||||
|
|
|
|||
|
|
@ -84,3 +84,13 @@
|
|||
sorting: 1
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 10
|
||||
project_id: 7
|
||||
title: Default
|
||||
creator_id: 2
|
||||
default: true
|
||||
sorting: 1
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
|
|
|||
|
|
@ -25,3 +25,24 @@
|
|||
project_id: 1
|
||||
project_board_id: 3
|
||||
sorting: 0
|
||||
|
||||
-
|
||||
id: 5
|
||||
issue_id: 16
|
||||
project_id: 4
|
||||
project_board_id: 4
|
||||
sorting: 0
|
||||
|
||||
-
|
||||
id: 6
|
||||
issue_id: 15
|
||||
project_id: 7
|
||||
project_board_id: 10
|
||||
sorting: 0
|
||||
|
||||
-
|
||||
id: 7
|
||||
issue_id: 17
|
||||
project_id: 7
|
||||
project_board_id: 10
|
||||
sorting: 1
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"forgejo.org/models/db"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
"forgejo.org/models/organization"
|
||||
project_model "forgejo.org/models/project"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
|
@ -538,6 +539,37 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
PageSize: setting.UI.IssuePagingNum,
|
||||
}
|
||||
|
||||
// Projects
|
||||
//
|
||||
// We do not consider listing projects for the individual repos.
|
||||
// Instead, we only support listing the projects under the scope of an individual/organisation.
|
||||
// However, this limitation does not affect filtering with a project ID.
|
||||
{
|
||||
projOpts := project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: ctxUser.ID,
|
||||
IsClosed: optional.None[bool](),
|
||||
Type: project_model.TypeIndividual,
|
||||
}
|
||||
if org != nil {
|
||||
projOpts.OwnerID = org.ID
|
||||
projOpts.Type = project_model.TypeOrganization
|
||||
}
|
||||
|
||||
projects, err := db.Find[project_model.Project](ctx, projOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
if projectID := ctx.FormInt64("project"); projectID != 0 {
|
||||
opts.ProjectID = projectID
|
||||
ctx.Data["ProjectID"] = projectID
|
||||
}
|
||||
|
||||
ctx.Data["Projects"] = projects
|
||||
}
|
||||
|
||||
// Get IDs for labels (a filter option for issues/pulls).
|
||||
// Required for IssuesOptions.
|
||||
selectedLabels := ctx.FormString("labels")
|
||||
|
|
@ -688,6 +720,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
pager.AddParam(ctx, "labels", "SelectLabels")
|
||||
pager.AddParam(ctx, "milestone", "MilestoneID")
|
||||
pager.AddParam(ctx, "assignee", "AssigneeID")
|
||||
pager.AddParam(ctx, "project", "ProjectID")
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplIssues)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
<div class="list-header">
|
||||
<div class="switch list-header-toggle">
|
||||
{{$keyword := StringUtils.RemoveAll $.Keyword "is:open" "-is:open" "is:closed" "-is:closed" "is:all"}}
|
||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&q={{$keyword}}">
|
||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&project={{$.ProjectID}}&state=open&labels={{.SelectLabels}}&q={{$keyword}}">
|
||||
{{svg "octicon-issue-opened" 16}}
|
||||
{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}}
|
||||
</a>
|
||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&q={{$keyword}}">
|
||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&project={{$.ProjectID}}&state=closed&labels={{.SelectLabels}}&q={{$keyword}}">
|
||||
{{svg "octicon-issue-closed" 16}}
|
||||
{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
|
||||
</a>
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
<div class="ui search fluid action input">
|
||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||
<input type="hidden" name="sort" value="{{$.SortType}}">
|
||||
<input type="hidden" name="project" value={{$.ProjectID}}>
|
||||
<input type="hidden" name="state" value="{{$.State}}">
|
||||
{{$placeholder := ctx.Locale.Tr "search.issue_kind"}}
|
||||
{{if .PageIsPulls}}
|
||||
|
|
@ -37,6 +38,26 @@
|
|||
{{if .PageIsOrgIssues}}
|
||||
{{template "shared/label_filter" .}}
|
||||
{{end}}
|
||||
<div class="list-header ui dropdown type jump item" data-test-tag="filter-project">
|
||||
<span class="text tw-whitespace-nowrap">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_project"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="ui menu">
|
||||
<a class="item" rel="nofollow" href="?type={{$.SortType}}&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_project_all"}}
|
||||
</a>
|
||||
<a class="item" rel="nofollow" href="?type={{$.SortType}}&sort={{$.SortType}}&project=-1&state={{$.State}}&q={{$.Keyword}}">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_project_none"}}
|
||||
</a>
|
||||
<div class="divider"></div>
|
||||
{{range $project := $.Projects}}
|
||||
<a class="{{if eq $.ProjectID $project.ID}}active {{end}}item" rel="nofollow" href="?type={{$.SortType}}&sort={{$.SortType}}&project={{$project.ID}}&state={{$.State}}&q={{$.Keyword}}">
|
||||
{{svg $project.IconName 16 "tw-mr-2 tw-shrink-0"}}{{$project.Title}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Type -->
|
||||
<div class="list-header ui dropdown type jump item">
|
||||
<span class="text tw-whitespace-nowrap">
|
||||
|
|
@ -44,29 +65,29 @@
|
|||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="ui menu">
|
||||
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="?type=created_by&sort={{$.SortType}}&project={{$.ProjectID}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<div class="ui circular mini label tw-ml-0">{{CountFmt .IssueStats.CreateCount}}</div>
|
||||
{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
|
||||
</a>
|
||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="?type=your_repositories&sort={{$.SortType}}&project={{$.ProjectID}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<div class="ui circular mini label tw-ml-0">{{CountFmt .IssueStats.YourRepositoriesCount}}</div>
|
||||
{{ctx.Locale.Tr "home.issues.in_your_repos"}}
|
||||
</a>
|
||||
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="?type=assigned&sort={{$.SortType}}&project={{$.ProjectID}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<div class="ui circular mini label tw-ml-0">{{CountFmt .IssueStats.AssignCount}}</div>
|
||||
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
|
||||
</a>
|
||||
{{if .PageIsPulls}}
|
||||
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="?type=review_requested&sort={{$.SortType}}&project={{$.ProjectID}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<div class="ui circular mini label tw-ml-0">{{CountFmt .IssueStats.ReviewRequestedCount}}</div>
|
||||
{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
|
||||
</a>
|
||||
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="?type=reviewed_by&sort={{$.SortType}}&project={{$.ProjectID}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<div class="ui circular mini label tw-ml-0">{{CountFmt .IssueStats.ReviewedCount}}</div>
|
||||
{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="?type=mentioned&sort={{$.SortType}}&project={{$.ProjectID}}&state={{.State}}&q={{$.Keyword}}">
|
||||
<div class="ui circular mini label tw-ml-0">{{CountFmt .IssueStats.MentionCount}}</div>
|
||||
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
|
||||
</a>
|
||||
|
|
@ -83,7 +104,7 @@
|
|||
{{$o := .}}
|
||||
{{range $opt := StringUtils.Make "recentupdate" "leastupdate" "latest" "oldest" "mostcomment" "leastcomment" "nearduedate" "farduedate"}}
|
||||
{{$text := ctx.Locale.Tr (printf "repo.issues.filter_sort.%s" $opt)}}
|
||||
<a class="{{if or (eq $o.SortType $opt) (and (eq $opt "latest") (not $o.SortType))}}active {{end}}item" href="?type={{$.ViewType}}&sort={{$opt}}&state={{$.State}}&labels={{$o.SelectLabels}}&q={{$keyword}}">{{
|
||||
<a class="{{if or (eq $o.SortType $opt) (and (eq $opt "latest") (not $o.SortType))}}active {{end}}item" href="?type={{$.ViewType}}&sort={{$opt}}&project={{$.ProjectID}}&state={{$.State}}&labels={{$o.SelectLabels}}&q={{$keyword}}">{{
|
||||
$text
|
||||
}}</a>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -1462,6 +1462,40 @@ func TestIssueOrgDashboard(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueDashboardProjects(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
testFn := func(t *testing.T, req *RequestWrapper, projectID int64) {
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
projectFilterHref, ok := htmlDoc.Find("[data-test-tag=filter-project] a.active").Attr("href")
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, projectFilterHref, fmt.Sprintf("project=%d", projectID))
|
||||
|
||||
issues := htmlDoc.Find("#issue-list .issue-meta")
|
||||
assert.NotZero(t, issues.Length())
|
||||
|
||||
issues.Each(func(i int, s *goquery.Selection) {
|
||||
issueProjectHref, ok := s.Find("a.project").Attr("href")
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, issueProjectHref, fmt.Sprintf("projects/%d", projectID))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("User", func(t *testing.T) {
|
||||
testFn(t, NewRequest(t, "GET", "/issues?project=4"), 4)
|
||||
})
|
||||
|
||||
t.Run("Org", func(t *testing.T) {
|
||||
testFn(t, NewRequestf(t, "GET", "/org/%s/issues?project=7", org.Name), 7)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssueCount(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue