Skip to content

Commit 2024519

Browse files
author
Earl Warren
committed
Merge pull request '[v7.0/forgejo] [FEAT] Add label filters in organization issues dashboard' (go-gitea#3038) from bp-v7.0/forgejo-8d13ed4 into v7.0/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3038 Reviewed-by: Gusted <[email protected]> Reviewed-by: Earl Warren <[email protected]>
2 parents 279d1a8 + cb0c6b5 commit 2024519

File tree

7 files changed

+169
-59
lines changed

7 files changed

+169
-59
lines changed

routers/web/user/home.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,36 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
538538
}
539539
}
540540

541+
if org != nil {
542+
// Get Org Labels
543+
labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
544+
if err != nil {
545+
ctx.ServerError("GetLabelsByOrgID", err)
546+
return
547+
}
548+
549+
// Get the exclusive scope for every label ID
550+
labelExclusiveScopes := make([]string, 0, len(opts.LabelIDs))
551+
for _, labelID := range opts.LabelIDs {
552+
foundExclusiveScope := false
553+
for _, label := range labels {
554+
if label.ID == labelID || label.ID == -labelID {
555+
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
556+
foundExclusiveScope = true
557+
break
558+
}
559+
}
560+
if !foundExclusiveScope {
561+
labelExclusiveScopes = append(labelExclusiveScopes, "")
562+
}
563+
}
564+
565+
for _, l := range labels {
566+
l.LoadSelectedLabelsAfterClick(opts.LabelIDs, labelExclusiveScopes)
567+
}
568+
ctx.Data["Labels"] = labels
569+
}
570+
541571
// ------------------------------
542572
// Get issues as defined by opts.
543573
// ------------------------------
@@ -621,6 +651,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
621651
ctx.Data["SortType"] = sortType
622652
ctx.Data["IsShowClosed"] = isShowClosed
623653
ctx.Data["SelectLabels"] = selectedLabels
654+
ctx.Data["PageIsOrgIssues"] = org != nil
624655

625656
if isShowClosed {
626657
ctx.Data["State"] = "closed"

routers/web/user/home_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"code.gitea.io/gitea/models/db"
11+
issues_model "code.gitea.io/gitea/models/issues"
1112
repo_model "code.gitea.io/gitea/models/repo"
1213
"code.gitea.io/gitea/models/unittest"
1314
"code.gitea.io/gitea/modules/setting"
@@ -130,3 +131,36 @@ func TestDashboardPagination(t *testing.T) {
130131
assert.NoError(t, err)
131132
assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`)
132133
}
134+
135+
func TestOrgLabels(t *testing.T) {
136+
assert.NoError(t, unittest.LoadFixtures())
137+
138+
ctx, _ := contexttest.MockContext(t, "org/org3/issues")
139+
contexttest.LoadUser(t, ctx, 2)
140+
contexttest.LoadOrganization(t, ctx, 3)
141+
Issues(ctx)
142+
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
143+
144+
assert.True(t, ctx.Data["PageIsOrgIssues"].(bool))
145+
146+
orgLabels := []struct {
147+
ID int64
148+
OrgID int64
149+
Name string
150+
}{
151+
{3, 3, "orglabel3"},
152+
{4, 3, "orglabel4"},
153+
}
154+
155+
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
156+
157+
assert.True(t, ok)
158+
159+
if assert.Len(t, labels, len(orgLabels)) {
160+
for i, label := range labels {
161+
assert.EqualValues(t, orgLabels[i].OrgID, label.OrgID)
162+
assert.EqualValues(t, orgLabels[i].ID, label.ID)
163+
assert.EqualValues(t, orgLabels[i].Name, label.Name)
164+
}
165+
}
166+
}

services/contexttest/context_tests.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"testing"
1616
"time"
1717

18+
org_model "code.gitea.io/gitea/models/organization"
1819
access_model "code.gitea.io/gitea/models/perm/access"
1920
repo_model "code.gitea.io/gitea/models/repo"
2021
"code.gitea.io/gitea/models/unittest"
@@ -146,6 +147,19 @@ func LoadUser(t *testing.T, ctx gocontext.Context, userID int64) {
146147
}
147148
}
148149

150+
// LoadOrganization load an org into a test context
151+
func LoadOrganization(t *testing.T, ctx gocontext.Context, orgID int64) {
152+
org := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: orgID})
153+
switch ctx := ctx.(type) {
154+
case *context.Context:
155+
ctx.Org.Organization = org
156+
case *context.APIContext:
157+
ctx.Org.Organization = org
158+
default:
159+
assert.FailNow(t, "context is not *context.Context or *context.APIContext")
160+
}
161+
}
162+
149163
// LoadGitRepo load a git repo into a test context. Requires that ctx.Repo has
150164
// already been populated.
151165
func LoadGitRepo(t *testing.T, ctx *context.Context) {

templates/repo/issue/filter_list.tmpl

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,5 @@
11
<!-- Label -->
2-
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter">
3-
<span class="text">
4-
{{ctx.Locale.Tr "repo.issues.filter_label"}}
5-
</span>
6-
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
7-
<div class="menu">
8-
<div class="ui icon search input">
9-
<i class="icon">{{svg "octicon-search" 16}}</i>
10-
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_label"}}">
11-
</div>
12-
<div class="ui checkbox compact archived-label-filter">
13-
<input name="archived" type="checkbox"
14-
id="archived-filter-checkbox"
15-
{{if .ShowArchivedLabels}}checked{{end}}
16-
>
17-
<label for="archived-filter-checkbox">
18-
{{ctx.Locale.Tr "repo.issues.label_archived_filter"}}
19-
<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
20-
{{svg "octicon-info"}}
21-
</i>
22-
</label>
23-
</div>
24-
<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
25-
<div class="divider"></div>
26-
<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>
27-
<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>
28-
{{$previousExclusiveScope := "_no_scope"}}
29-
{{range .Labels}}
30-
{{$exclusiveScope := .ExclusiveScope}}
31-
{{if and (ne $previousExclusiveScope $exclusiveScope)}}
32-
<div class="divider"></div>
33-
{{end}}
34-
{{$previousExclusiveScope = $exclusiveScope}}
35-
<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}}">
36-
{{if .IsExcluded}}
37-
{{svg "octicon-circle-slash"}}
38-
{{else if .IsSelected}}
39-
{{if $exclusiveScope}}
40-
{{svg "octicon-dot-fill"}}
41-
{{else}}
42-
{{svg "octicon-check"}}
43-
{{end}}
44-
{{end}}
45-
{{RenderLabel $.Context ctx.Locale .}}
46-
<p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
47-
</a>
48-
{{end}}
49-
</div>
50-
</div>
2+
{{template "shared/label_filter" .}}
513

524
{{if not .Milestone}}
535
<!-- Milestone -->

templates/shared/label_filter.tmpl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!-- Label -->
2+
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter">
3+
<span class="text">
4+
{{ctx.Locale.Tr "repo.issues.filter_label"}}
5+
</span>
6+
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
7+
<div class="menu">
8+
<div class="ui icon search input">
9+
<i class="icon">{{svg "octicon-search" 16}}</i>
10+
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_label"}}">
11+
</div>
12+
<div class="ui checkbox compact archived-label-filter">
13+
<input name="archived" type="checkbox"
14+
id="archived-filter-checkbox"
15+
{{if .ShowArchivedLabels}}checked{{end}}
16+
>
17+
<label for="archived-filter-checkbox">
18+
{{ctx.Locale.Tr "repo.issues.label_archived_filter"}}
19+
<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
20+
{{svg "octicon-info"}}
21+
</i>
22+
</label>
23+
</div>
24+
<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
25+
<div class="divider"></div>
26+
<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>
27+
<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>
28+
{{$previousExclusiveScope := "_no_scope"}}
29+
{{range .Labels}}
30+
{{$exclusiveScope := .ExclusiveScope}}
31+
{{if and (ne $previousExclusiveScope $exclusiveScope)}}
32+
<div class="divider"></div>
33+
{{end}}
34+
{{$previousExclusiveScope = $exclusiveScope}}
35+
<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}}">
36+
{{if .IsExcluded}}
37+
{{svg "octicon-circle-slash"}}
38+
{{else if .IsSelected}}
39+
{{if $exclusiveScope}}
40+
{{svg "octicon-dot-fill"}}
41+
{{else}}
42+
{{svg "octicon-check"}}
43+
{{end}}
44+
{{end}}
45+
{{RenderLabel $.Context ctx.Locale .}}
46+
<p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
47+
</a>
48+
{{end}}
49+
</div>
50+
</div>

templates/user/dashboard/issues.tmpl

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
<div class="flex-container-main content">
3838
<div class="list-header">
3939
<div class="small-menu-items ui compact tiny menu list-header-toggle">
40-
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
40+
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&q={{$.Keyword}}">
4141
{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
4242
{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
4343
</a>
44-
<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
44+
<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&q={{$.Keyword}}">
4545
{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
4646
{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
4747
</a>
@@ -56,21 +56,25 @@
5656
{{template "shared/search/button"}}
5757
</div>
5858
</form>
59+
<!-- Label -->
60+
{{if .PageIsOrgIssues}}
61+
{{template "shared/label_filter" .}}
62+
{{end}}
5963
<!-- Sort -->
6064
<div class="list-header-sort ui small dropdown type jump item">
6165
<span class="text tw-whitespace-nowrap">
6266
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
6367
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
6468
</span>
6569
<div class="menu">
66-
<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>
67-
<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>
68-
<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>
69-
<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>
70-
<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>
71-
<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>
72-
<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>
73-
<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>
70+
<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>
71+
<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>
72+
<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>
73+
<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>
74+
<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>
75+
<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>
76+
<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>
77+
<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>
7478
</div>
7579
</div>
7680
</div>

tests/integration/org_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,28 @@ func TestTeamSearch(t *testing.T) {
222222
req.Header.Add("X-Csrf-Token", csrf)
223223
session.MakeRequest(t, req, http.StatusNotFound)
224224
}
225+
226+
func TestOrgDashboardLabels(t *testing.T) {
227+
defer tests.PrepareTestEnv(t)()
228+
229+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
230+
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
231+
session := loginUser(t, user.Name)
232+
233+
req := NewRequestf(t, "GET", "/org/%s/issues?labels=3,4", org.Name)
234+
resp := session.MakeRequest(t, req, http.StatusOK)
235+
htmlDoc := NewHTMLParser(t, resp.Body)
236+
237+
labelFilterHref, ok := htmlDoc.Find(".list-header-sort a").Attr("href")
238+
assert.True(t, ok)
239+
assert.Contains(t, labelFilterHref, "labels=3%2c4")
240+
241+
// Exclude label
242+
req = NewRequestf(t, "GET", "/org/%s/issues?labels=3,-4", org.Name)
243+
resp = session.MakeRequest(t, req, http.StatusOK)
244+
htmlDoc = NewHTMLParser(t, resp.Body)
245+
246+
labelFilterHref, ok = htmlDoc.Find(".list-header-sort a").Attr("href")
247+
assert.True(t, ok)
248+
assert.Contains(t, labelFilterHref, "labels=3%2c-4")
249+
}

0 commit comments

Comments
 (0)