loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[FEAT] Add label filters in organization issues dashboard' (#2944) from iminfinity/forgejo:add/label-filters into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2944
Reviewed-by: Gusted <gusted@noreply.codeberg.org>

+169 -59
+31
routers/web/user/home.go
··· 538 538 } 539 539 } 540 540 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 + 541 571 // ------------------------------ 542 572 // Get issues as defined by opts. 543 573 // ------------------------------ ··· 621 651 ctx.Data["SortType"] = sortType 622 652 ctx.Data["IsShowClosed"] = isShowClosed 623 653 ctx.Data["SelectLabels"] = selectedLabels 654 + ctx.Data["PageIsOrgIssues"] = org != nil 624 655 625 656 if isShowClosed { 626 657 ctx.Data["State"] = "closed"
+34
routers/web/user/home_test.go
··· 8 8 "testing" 9 9 10 10 "code.gitea.io/gitea/models/db" 11 + issues_model "code.gitea.io/gitea/models/issues" 11 12 repo_model "code.gitea.io/gitea/models/repo" 12 13 "code.gitea.io/gitea/models/unittest" 13 14 "code.gitea.io/gitea/modules/setting" ··· 130 131 assert.NoError(t, err) 131 132 assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`) 132 133 } 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 + }
+14
services/contexttest/context_tests.go
··· 15 15 "testing" 16 16 "time" 17 17 18 + org_model "code.gitea.io/gitea/models/organization" 18 19 access_model "code.gitea.io/gitea/models/perm/access" 19 20 repo_model "code.gitea.io/gitea/models/repo" 20 21 "code.gitea.io/gitea/models/unittest" ··· 141 142 ctx.Doer = doer 142 143 case *context.APIContext: 143 144 ctx.Doer = doer 145 + default: 146 + assert.FailNow(t, "context is not *context.Context or *context.APIContext") 147 + } 148 + } 149 + 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 144 158 default: 145 159 assert.FailNow(t, "context is not *context.Context or *context.APIContext") 146 160 }
+1 -49
templates/repo/issue/filter_list.tmpl
··· 1 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}}&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" .}} 51 3 52 4 {{if not .Milestone}} 53 5 <!-- Milestone -->
+50
templates/shared/label_filter.tmpl
··· 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>
+14 -10
templates/user/dashboard/issues.tmpl
··· 37 37 <div class="flex-container-main content"> 38 38 <div class="list-header"> 39 39 <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}}"> 41 41 {{svg "octicon-issue-opened" 16 "tw-mr-2"}} 42 42 {{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}} 43 43 </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}}"> 45 45 {{svg "octicon-issue-closed" 16 "tw-mr-2"}} 46 46 {{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}} 47 47 </a> ··· 56 56 {{template "shared/search/button"}} 57 57 </div> 58 58 </form> 59 + <!-- Label --> 60 + {{if .PageIsOrgIssues}} 61 + {{template "shared/label_filter" .}} 62 + {{end}} 59 63 <!-- Sort --> 60 64 <div class="list-header-sort ui small dropdown type jump item"> 61 65 <span class="text tw-whitespace-nowrap"> ··· 63 67 {{svg "octicon-triangle-down" 14 "dropdown icon"}} 64 68 </span> 65 69 <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> 74 78 </div> 75 79 </div> 76 80 </div>
+25
tests/integration/org_test.go
··· 222 222 req.Header.Add("X-Csrf-Token", csrf) 223 223 session.MakeRequest(t, req, http.StatusNotFound) 224 224 } 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 + }