Skip to content

Commit cb6a227

Browse files
committed
add filter by author/assignee in /issues
1 parent e91d4f1 commit cb6a227

File tree

6 files changed

+152
-80
lines changed

6 files changed

+152
-80
lines changed

models/issues/issue_stats.go

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333
FilterModeMention
3434
FilterModeReviewRequested
3535
FilterModeReviewed
36+
FilterModeSearch
3637
FilterModeYourRepositories
3738
)
3839

models/repo/user_repo.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,17 @@ func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull boo
163163
if isShowFullName {
164164
prefixCond = prefixCond.Or(builder.Like{"full_name", "%" + search + "%"})
165165
}
166-
167-
cond := builder.In("`user`.id",
168-
builder.Select("poster_id").From("issue").Where(
169-
builder.Eq{"repo_id": repo.ID}.
170-
And(builder.Eq{"is_pull": isPull}),
171-
).GroupBy("poster_id")).And(prefixCond)
166+
var cond builder.Cond
167+
if repo != nil {
168+
cond = builder.In("`user`.id",
169+
builder.Select("poster_id").From("issue").Where(
170+
builder.Eq{"repo_id": repo.ID}.
171+
And(builder.Eq{"is_pull": isPull}),
172+
).GroupBy("poster_id")).And(prefixCond)
173+
} else {
174+
cond = builder.In("`user`.id",
175+
builder.Select("poster_id").From("issue").GroupBy("poster_id")).And(prefixCond)
176+
}
172177

173178
return users, db.GetEngine(ctx).
174179
Where(cond).

routers/web/user/home.go

+23-8
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
403403
filterMode = issues_model.FilterModeReviewRequested
404404
case "reviewed_by":
405405
filterMode = issues_model.FilterModeReviewed
406+
case "search":
407+
filterMode = issues_model.FilterModeSearch
406408
case "your_repositories":
407409
fallthrough
408410
default:
@@ -476,6 +478,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
476478
}
477479
}
478480

481+
// Get filter by author id
482+
filterByAuthorID, errorParsingAuthorID := strconv.ParseInt(ctx.FormString("author"), 10, 64)
483+
if errorParsingAuthorID != nil {
484+
filterByAuthorID = 0
485+
}
486+
487+
// Get filter by assignee id
488+
filterByAssigneeID, errorParsingAssigneeID := strconv.ParseInt(ctx.FormString("assignee"), 10, 64)
489+
if errorParsingAssigneeID != nil {
490+
filterByAssigneeID = 0
491+
}
492+
479493
switch filterMode {
480494
case issues_model.FilterModeAll:
481495
case issues_model.FilterModeYourRepositories:
@@ -489,6 +503,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
489503
opts.ReviewRequestedID = ctx.Doer.ID
490504
case issues_model.FilterModeReviewed:
491505
opts.ReviewedID = ctx.Doer.ID
506+
case issues_model.FilterModeSearch:
507+
opts.PosterID = filterByAuthorID
508+
opts.AssigneeID = filterByAssigneeID
492509
}
493510

494511
// keyword holds the search term entered into the search field.
@@ -682,6 +699,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
682699
ctx.Data["RepoIDs"] = selectedRepoIDs
683700
ctx.Data["IsShowClosed"] = isShowClosed
684701
ctx.Data["SelectLabels"] = selectedLabels
702+
ctx.Data["FilterByAuthorID"] = filterByAuthorID
703+
ctx.Data["FilterByAssigneeID"] = filterByAssigneeID
685704

686705
if isShowClosed {
687706
ctx.Data["State"] = "closed"
@@ -703,6 +722,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
703722
pager.AddParam(ctx, "labels", "SelectLabels")
704723
pager.AddParam(ctx, "milestone", "MilestoneID")
705724
pager.AddParam(ctx, "assignee", "AssigneeID")
725+
pager.AddParam(ctx, "author", "AuthorID")
706726
ctx.Data["Page"] = pager
707727

708728
ctx.HTML(http.StatusOK, tplIssues)
@@ -864,14 +884,6 @@ func UsernameSubRoute(ctx *context.Context) {
864884
}
865885

866886
func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions, doerID int64) (*issues_model.IssueStats, error) {
867-
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
868-
o.AssigneeID = nil
869-
o.PosterID = nil
870-
o.MentionID = nil
871-
o.ReviewRequestedID = nil
872-
o.ReviewedID = nil
873-
})
874-
875887
var (
876888
err error
877889
ret = &issues_model.IssueStats{}
@@ -891,6 +903,9 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer
891903
openClosedOpts.ReviewRequestedID = &doerID
892904
case issues_model.FilterModeReviewed:
893905
openClosedOpts.ReviewedID = &doerID
906+
case issues_model.FilterModeSearch:
907+
openClosedOpts.PosterID = opts.PosterID
908+
openClosedOpts.AssigneeID = opts.AssigneeID
894909
}
895910
openClosedOpts.IsClosed = util.OptionalBoolFalse
896911
ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)

routers/web/web.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -478,10 +478,15 @@ func registerRoutes(m *web.Route) {
478478
}, ignExploreSignIn)
479479
m.Group("/issues", func() {
480480
m.Get("", user.Issues)
481+
m.Get("/posters", repo.IssuePosters)
482+
m.Get("/filter", user.Issues)
481483
m.Get("/search", repo.SearchIssues)
482484
}, reqSignIn)
483485

484-
m.Get("/pulls", reqSignIn, user.Pulls)
486+
m.Group("/pulls", func() {
487+
m.Get("", user.Pulls)
488+
m.Get("/posters", repo.IssuePosters)
489+
}, reqSignIn)
485490
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
486491

487492
// ***** START: User *****

templates/user/dashboard/issues.tmpl

+51-9
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
11
{{template "base/head" .}}
22
<div role="main" aria-label="{{.Title}}" class="page-content dashboard issues">
33
{{template "user/dashboard/navbar" .}}
4+
45
<div class="ui container">
56
<div class="ui stackable grid">
67
<div class="four wide column">
78
<div class="ui secondary vertical filter menu gt-bg-transparent">
8-
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
9+
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
910
{{ctx.Locale.Tr "home.issues.in_your_repos"}}
1011
<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
1112
</a>
12-
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
13+
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
1314
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
1415
<strong>{{CountFmt .IssueStats.AssignCount}}</strong>
1516
</a>
16-
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
17+
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
1718
{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
1819
<strong>{{CountFmt .IssueStats.CreateCount}}</strong>
1920
</a>
2021
{{if .PageIsPulls}}
21-
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
22+
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
2223
{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
2324
<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
2425
</a>
25-
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
26+
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
2627
{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
2728
<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
2829
</a>
2930
{{end}}
30-
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
31+
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
3132
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
3233
<strong>{{CountFmt .IssueStats.MentionCount}}</strong>
3334
</a>
3435
<div class="divider"></div>
35-
<a class="{{if not $.RepoIDs}}active{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}">
36+
<a class="{{if not $.RepoIDs}}active{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
3637
<span class="text truncate">{{ctx.Locale.Tr "all"}}</span>
3738
<span>{{CountFmt .TotalIssueCount}}</span>
3839
</a>
@@ -62,11 +63,11 @@
6263
<div class="twelve wide column content">
6364
<div class="list-header">
6465
<div class="small-menu-items ui compact tiny menu list-header-toggle">
65-
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
66+
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
6667
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
6768
{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
6869
</a>
69-
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
70+
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}&assignee={{$.FilterByAssigneeID}}&author={{$.FilterByAuthorID}}">
7071
{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
7172
{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
7273
</a>
@@ -106,7 +107,48 @@
106107
<a class="ui primary button gt-ml-4" href="{{.SingleRepoLink}}/compare">{{ctx.Locale.Tr "repo.pulls.new"}}</a>
107108
{{end}}
108109
{{end}}
110+
<div id="issue-filters" class="issue-list-toolbar-right">
111+
<div class="ui secondary filter menu labels">
112+
<!-- Author -->
113+
<div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{.locale.Tr "repo.author_search_tooltip"}}"
114+
data-search-url="{{$.Link}}/posters"
115+
data-selected-user-id="{{$.PosterID}}"
116+
data-action-jump-url="{{$.Link}}?type=search&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&assignee={{$.FilterByAssigneeID}}&author={user_id}"
117+
>
118+
<span class="text">
119+
{{.locale.Tr "repo.issues.filter_poster"}}
120+
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
121+
</span>
122+
<div class="menu">
123+
<div class="ui icon search input">
124+
<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
125+
<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_poster"}}">
126+
</div>
127+
<a class="item" data-value="0">{{.locale.Tr "repo.issues.filter_poster_no_select"}}</a>
128+
</div>
129+
</div>
130+
<!-- Assignee -->
131+
<div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{.locale.Tr "repo.author_search_tooltip"}}"
132+
data-search-url="{{$.Link}}/posters"
133+
data-selected-user-id="{{$.PosterID}}"
134+
data-action-jump-url="{{$.Link}}?type=search&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&assignee={user_id}&author={{$.FilterByAuthorID}}"
135+
>
136+
<span class="text">
137+
{{.locale.Tr "repo.issues.filter_assignee"}}
138+
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
139+
</span>
140+
<div class="menu">
141+
<div class="ui icon search input">
142+
<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
143+
<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_assignee"}}">
144+
</div>
145+
<a class="item" data-value="0">{{.locale.Tr "repo.issues.filter_assginee_no_select"}}</a>
146+
</div>
147+
</div>
148+
</div>
149+
</div>
109150
</div>
151+
110152
{{template "shared/issuelist" dict "." . "listType" "dashboard"}}
111153
</div>
112154
</div>

web_src/js/features/repo-issue-list.js

+60-56
Original file line numberDiff line numberDiff line change
@@ -82,62 +82,66 @@ function initRepoIssueListCheckboxes() {
8282
});
8383
}
8484

85-
function initRepoIssueListAuthorDropdown() {
86-
const $searchDropdown = $('.user-remote-search');
87-
if (!$searchDropdown.length) return;
88-
89-
let searchUrl = $searchDropdown.attr('data-search-url');
90-
const actionJumpUrl = $searchDropdown.attr('data-action-jump-url');
91-
const selectedUserId = $searchDropdown.attr('data-selected-user-id');
92-
if (!searchUrl.includes('?')) searchUrl += '?';
93-
94-
$searchDropdown.dropdown('setting', {
95-
fullTextSearch: true,
96-
selectOnKeydown: false,
97-
apiSettings: {
98-
cache: false,
99-
url: `${searchUrl}&q={query}`,
100-
onResponse(resp) {
101-
// the content is provided by backend IssuePosters handler
102-
const processedResults = []; // to be used by dropdown to generate menu items
103-
for (const item of resp.results) {
104-
let html = `<img class="ui avatar gt-vm" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
105-
if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`;
106-
processedResults.push({value: item.user_id, name: html});
107-
}
108-
resp.results = processedResults;
109-
return resp;
85+
function initRepoIssueListUserDropdowns() {
86+
const userDropdowns = document.getElementsByClassName('user-remote-search');
87+
if (!userDropdowns.length) return;
88+
89+
for (let i = 0; i < userDropdowns.length; i++) {
90+
const $searchDropdown = $(userDropdowns[i]);
91+
92+
let searchUrl = $searchDropdown.attr('data-search-url');
93+
const actionJumpUrl = $searchDropdown.attr('data-action-jump-url');
94+
const selectedUserId = $searchDropdown.attr('data-selected-user-id');
95+
if (!searchUrl.includes('?')) searchUrl += '?';
96+
97+
$searchDropdown.dropdown('setting', {
98+
fullTextSearch: true,
99+
selectOnKeydown: false,
100+
apiSettings: {
101+
cache: false,
102+
url: `${searchUrl}&q={query}`,
103+
onResponse(resp) {
104+
// the content is provided by backend IssuePosters handler
105+
const processedResults = []; // to be used by dropdown to generate menu items
106+
for (const item of resp.results) {
107+
let html = `<img class="ui avatar gt-vm" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
108+
if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`;
109+
processedResults.push({value: item.user_id, name: html});
110+
}
111+
resp.results = processedResults;
112+
return resp;
113+
},
110114
},
111-
},
112-
action: (_text, value) => {
113-
window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value));
114-
},
115-
onShow: () => {
116-
$searchDropdown.dropdown('filter', ' '); // trigger a search on first show
117-
},
118-
});
115+
action: (_text, value) => {
116+
window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value));
117+
},
118+
onShow: () => {
119+
$searchDropdown.dropdown('filter', ' '); // trigger a search on first show
120+
},
121+
});
119122

120-
// we want to generate the dropdown menu items by ourselves, replace its internal setup functions
121-
const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')};
122-
const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
123-
$searchDropdown.dropdown('internal', 'setup', dropdownSetup);
124-
dropdownSetup.menu = function (values) {
125-
const $menu = $searchDropdown.find('> .menu');
126-
$menu.find('> .dynamic-item').remove(); // remove old dynamic items
127-
128-
const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className'));
129-
if (newMenuHtml) {
130-
const $newMenuItems = $(newMenuHtml);
131-
$newMenuItems.addClass('dynamic-item');
132-
$menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems);
133-
}
134-
$searchDropdown.dropdown('refresh');
135-
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
136-
setTimeout(() => {
137-
$menu.find('.item.active, .item.selected').removeClass('active selected');
138-
$menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected');
139-
}, 0);
140-
};
123+
// we want to generate the dropdown menu items by ourselves, replace its internal setup functions
124+
const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')};
125+
const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
126+
$searchDropdown.dropdown('internal', 'setup', dropdownSetup);
127+
dropdownSetup.menu = function (values) {
128+
const $menu = $searchDropdown.find('> .menu');
129+
$menu.find('> .dynamic-item').remove(); // remove old dynamic items
130+
131+
const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className'));
132+
if (newMenuHtml) {
133+
const $newMenuItems = $(newMenuHtml);
134+
$newMenuItems.addClass('dynamic-item');
135+
$menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems);
136+
}
137+
$searchDropdown.dropdown('refresh');
138+
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
139+
setTimeout(() => {
140+
$menu.find('.item.active, .item.selected').removeClass('active selected');
141+
$menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected');
142+
}, 0);
143+
};
144+
}
141145
}
142146

143147
function initPinRemoveButton() {
@@ -222,9 +226,9 @@ function initArchivedLabelFilter() {
222226
}
223227

224228
export function initRepoIssueList() {
225-
if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list').length) return;
229+
if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list, .page-content.dashboard.issues').length) return;
226230
initRepoIssueListCheckboxes();
227-
initRepoIssueListAuthorDropdown();
231+
initRepoIssueListUserDropdowns();
228232
initIssuePinSort();
229233
initArchivedLabelFilter();
230234
}

0 commit comments

Comments
 (0)