summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoriminfinity <iminfinity@pm.me>2024-04-04 14:27:11 +0200
committeriminfinity <iminfinity@pm.me>2024-04-04 14:27:11 +0200
commit8d13ed4a8d03a7542641faf771dad56783ab3a4c (patch)
treeccab1bfee001df276f75c3e0632f57ffd20b492a
parentMerge pull request '[BUG] Center icon and callout text' (#3010) from gusted/f... (diff)
downloadforgejo-8d13ed4a8d03a7542641faf771dad56783ab3a4c.tar.xz
forgejo-8d13ed4a8d03a7542641faf771dad56783ab3a4c.zip
add label filters in org/{org_name}/issues
-rw-r--r--routers/web/user/home.go31
-rw-r--r--routers/web/user/home_test.go34
-rw-r--r--services/contexttest/context_tests.go14
-rw-r--r--templates/repo/issue/filter_list.tmpl50
-rw-r--r--templates/shared/label_filter.tmpl50
-rw-r--r--templates/user/dashboard/issues.tmpl24
-rw-r--r--tests/integration/org_test.go25
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}}&nbsp;{{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}}&nbsp;{{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")
+}