diff options
author | iminfinity <iminfinity@pm.me> | 2024-04-04 14:27:11 +0200 |
---|---|---|
committer | iminfinity <iminfinity@pm.me> | 2024-04-04 14:27:11 +0200 |
commit | 8d13ed4a8d03a7542641faf771dad56783ab3a4c (patch) | |
tree | ccab1bfee001df276f75c3e0632f57ffd20b492a | |
parent | Merge pull request '[BUG] Center icon and callout text' (#3010) from gusted/f... (diff) | |
download | forgejo-8d13ed4a8d03a7542641faf771dad56783ab3a4c.tar.xz forgejo-8d13ed4a8d03a7542641faf771dad56783ab3a4c.zip |
add label filters in org/{org_name}/issues
-rw-r--r-- | routers/web/user/home.go | 31 | ||||
-rw-r--r-- | routers/web/user/home_test.go | 34 | ||||
-rw-r--r-- | services/contexttest/context_tests.go | 14 | ||||
-rw-r--r-- | templates/repo/issue/filter_list.tmpl | 50 | ||||
-rw-r--r-- | templates/shared/label_filter.tmpl | 50 | ||||
-rw-r--r-- | templates/user/dashboard/issues.tmpl | 24 | ||||
-rw-r--r-- | tests/integration/org_test.go | 25 |
7 files changed, 169 insertions, 59 deletions
diff --git a/routers/web/user/home.go b/routers/web/user/home.go index f122dc5d9c..52aca1825a 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -538,6 +538,36 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } } + if org != nil { + // Get Org Labels + labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByOrgID", err) + return + } + + // Get the exclusive scope for every label ID + labelExclusiveScopes := make([]string, 0, len(opts.LabelIDs)) + for _, labelID := range opts.LabelIDs { + foundExclusiveScope := false + for _, label := range labels { + if label.ID == labelID || label.ID == -labelID { + labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) + foundExclusiveScope = true + break + } + } + if !foundExclusiveScope { + labelExclusiveScopes = append(labelExclusiveScopes, "") + } + } + + for _, l := range labels { + l.LoadSelectedLabelsAfterClick(opts.LabelIDs, labelExclusiveScopes) + } + ctx.Data["Labels"] = labels + } + // ------------------------------ // Get issues as defined by opts. // ------------------------------ @@ -621,6 +651,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["SortType"] = sortType ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["SelectLabels"] = selectedLabels + ctx.Data["PageIsOrgIssues"] = org != nil if isShowClosed { ctx.Data["State"] = "closed" diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 1cc9886308..a59afce12c 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -130,3 +131,36 @@ func TestDashboardPagination(t *testing.T) { assert.NoError(t, err) assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`) } + +func TestOrgLabels(t *testing.T) { + assert.NoError(t, unittest.LoadFixtures()) + + ctx, _ := contexttest.MockContext(t, "org/org3/issues") + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadOrganization(t, ctx, 3) + Issues(ctx) + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + + assert.True(t, ctx.Data["PageIsOrgIssues"].(bool)) + + orgLabels := []struct { + ID int64 + OrgID int64 + Name string + }{ + {3, 3, "orglabel3"}, + {4, 3, "orglabel4"}, + } + + labels, ok := ctx.Data["Labels"].([]*issues_model.Label) + + assert.True(t, ok) + + if assert.Len(t, labels, len(orgLabels)) { + for i, label := range labels { + assert.EqualValues(t, orgLabels[i].OrgID, label.OrgID) + assert.EqualValues(t, orgLabels[i].ID, label.ID) + assert.EqualValues(t, orgLabels[i].Name, label.Name) + } + } +} diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index d3e6de7efe..7bfab2ed16 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -15,6 +15,7 @@ import ( "testing" "time" + org_model "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -146,6 +147,19 @@ func LoadUser(t *testing.T, ctx gocontext.Context, userID int64) { } } +// LoadOrganization load an org into a test context +func LoadOrganization(t *testing.T, ctx gocontext.Context, orgID int64) { + org := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: orgID}) + switch ctx := ctx.(type) { + case *context.Context: + ctx.Org.Organization = org + case *context.APIContext: + ctx.Org.Organization = org + default: + assert.FailNow(t, "context is not *context.Context or *context.APIContext") + } +} + // LoadGitRepo load a git repo into a test context. Requires that ctx.Repo has // already been populated. func LoadGitRepo(t *testing.T, ctx *context.Context) { diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 997557c45e..f60389766e 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -1,53 +1,5 @@ <!-- Label --> -<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter"> - <span class="text"> - {{ctx.Locale.Tr "repo.issues.filter_label"}} - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - <div class="ui icon search input"> - <i class="icon">{{svg "octicon-search" 16}}</i> - <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_label"}}"> - </div> - <div class="ui checkbox compact archived-label-filter"> - <input name="archived" type="checkbox" - id="archived-filter-checkbox" - {{if .ShowArchivedLabels}}checked{{end}} - > - <label for="archived-filter-checkbox"> - {{ctx.Locale.Tr "repo.issues.label_archived_filter"}} - <i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}> - {{svg "octicon-info"}} - </i> - </label> - </div> - <span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span> - <div class="divider"></div> - <a rel="nofollow" class="{{if .AllLabels}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a> - <a rel="nofollow" class="{{if .NoLabel}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> - {{$previousExclusiveScope := "_no_scope"}} - {{range .Labels}} - {{$exclusiveScope := .ExclusiveScope}} - {{if and (ne $previousExclusiveScope $exclusiveScope)}} - <div class="divider"></div> - {{end}} - {{$previousExclusiveScope = $exclusiveScope}} - <a rel="nofollow" class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}"> - {{if .IsExcluded}} - {{svg "octicon-circle-slash"}} - {{else if .IsSelected}} - {{if $exclusiveScope}} - {{svg "octicon-dot-fill"}} - {{else}} - {{svg "octicon-check"}} - {{end}} - {{end}} - {{RenderLabel $.Context ctx.Locale .}} - <p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p> - </a> - {{end}} - </div> -</div> +{{template "shared/label_filter" .}} {{if not .Milestone}} <!-- Milestone --> diff --git a/templates/shared/label_filter.tmpl b/templates/shared/label_filter.tmpl new file mode 100644 index 0000000000..9daeb3f100 --- /dev/null +++ b/templates/shared/label_filter.tmpl @@ -0,0 +1,50 @@ +<!-- Label --> +<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter"> + <span class="text"> + {{ctx.Locale.Tr "repo.issues.filter_label"}} + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + <div class="ui icon search input"> + <i class="icon">{{svg "octicon-search" 16}}</i> + <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_label"}}"> + </div> + <div class="ui checkbox compact archived-label-filter"> + <input name="archived" type="checkbox" + id="archived-filter-checkbox" + {{if .ShowArchivedLabels}}checked{{end}} + > + <label for="archived-filter-checkbox"> + {{ctx.Locale.Tr "repo.issues.label_archived_filter"}} + <i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}> + {{svg "octicon-info"}} + </i> + </label> + </div> + <span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span> + <div class="divider"></div> + <a rel="nofollow" class="{{if .AllLabels}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a> + <a rel="nofollow" class="{{if .NoLabel}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> + {{$previousExclusiveScope := "_no_scope"}} + {{range .Labels}} + {{$exclusiveScope := .ExclusiveScope}} + {{if and (ne $previousExclusiveScope $exclusiveScope)}} + <div class="divider"></div> + {{end}} + {{$previousExclusiveScope = $exclusiveScope}} + <a rel="nofollow" class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&labels={{.QueryString}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}"> + {{if .IsExcluded}} + {{svg "octicon-circle-slash"}} + {{else if .IsSelected}} + {{if $exclusiveScope}} + {{svg "octicon-dot-fill"}} + {{else}} + {{svg "octicon-check"}} + {{end}} + {{end}} + {{RenderLabel $.Context ctx.Locale .}} + <p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p> + </a> + {{end}} + </div> +</div> diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 89f23163f7..b7cc54091c 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -37,11 +37,11 @@ <div class="flex-container-main content"> <div class="list-header"> <div class="small-menu-items ui compact tiny menu list-header-toggle"> - <a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> + <a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&q={{$.Keyword}}"> {{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{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&q={{$.Keyword}}"> + <a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&q={{$.Keyword}}"> {{svg "octicon-issue-closed" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> @@ -56,6 +56,10 @@ {{template "shared/search/button"}} </div> </form> + <!-- Label --> + {{if .PageIsOrgIssues}} + {{template "shared/label_filter" .}} + {{end}} <!-- Sort --> <div class="list-header-sort ui small dropdown type jump item"> <span class="text tw-whitespace-nowrap"> @@ -63,14 +67,14 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}} </span> <div class="menu"> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> - <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> - <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> - <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> + <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> + <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> + <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> </div> </div> </div> diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go index 94c4e19727..a1e448be8a 100644 --- a/tests/integration/org_test.go +++ b/tests/integration/org_test.go @@ -222,3 +222,28 @@ func TestTeamSearch(t *testing.T) { req.Header.Add("X-Csrf-Token", csrf) session.MakeRequest(t, req, http.StatusNotFound) } + +func TestOrgDashboardLabels(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization}) + session := loginUser(t, user.Name) + + req := NewRequestf(t, "GET", "/org/%s/issues?labels=3,4", org.Name) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + labelFilterHref, ok := htmlDoc.Find(".list-header-sort a").Attr("href") + assert.True(t, ok) + assert.Contains(t, labelFilterHref, "labels=3%2c4") + + // Exclude label + req = NewRequestf(t, "GET", "/org/%s/issues?labels=3,-4", org.Name) + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + labelFilterHref, ok = htmlDoc.Find(".list-header-sort a").Attr("href") + assert.True(t, ok) + assert.Contains(t, labelFilterHref, "labels=3%2c-4") +} |