Skip to content

Commit 23471e1

Browse files
authored
Refactor issue list (#32755)
1. add backend support for filtering "poster" and "assignee" * due to the limits, there is no frontend support at the moment 2. rewrite TS code without jquery, now there are 14 jQuery files left:
1 parent 9d08d3f commit 23471e1

File tree

8 files changed

+246
-155
lines changed

8 files changed

+246
-155
lines changed

modules/templates/helper.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func NewFuncMap() template.FuncMap {
4242
"HTMLFormat": htmlutil.HTMLFormat,
4343
"HTMLEscape": htmlEscape,
4444
"QueryEscape": queryEscape,
45+
"QueryBuild": queryBuild,
4546
"JSEscape": jsEscapeSafe,
4647
"SanitizeHTML": SanitizeHTML,
4748
"URLJoin": util.URLJoin,
@@ -293,6 +294,71 @@ func timeEstimateString(timeSec any) string {
293294
return util.TimeEstimateString(v)
294295
}
295296

297+
type QueryString string
298+
299+
func queryBuild(a ...any) QueryString {
300+
var s string
301+
if len(a)%2 == 1 {
302+
if v, ok := a[0].(string); ok {
303+
if v == "" || (v[0] != '?' && v[0] != '&') {
304+
panic("queryBuild: invalid argument")
305+
}
306+
s = v
307+
} else if v, ok := a[0].(QueryString); ok {
308+
s = string(v)
309+
} else {
310+
panic("queryBuild: invalid argument")
311+
}
312+
}
313+
for i := len(a) % 2; i < len(a); i += 2 {
314+
k, ok := a[i].(string)
315+
if !ok {
316+
panic("queryBuild: invalid argument")
317+
}
318+
var v string
319+
if va, ok := a[i+1].(string); ok {
320+
v = va
321+
} else if a[i+1] != nil {
322+
v = fmt.Sprint(a[i+1])
323+
}
324+
// pos1 to pos2 is the "k=v&" part, "&" is optional
325+
pos1 := strings.Index(s, "&"+k+"=")
326+
if pos1 != -1 {
327+
pos1++
328+
} else {
329+
pos1 = strings.Index(s, "?"+k+"=")
330+
if pos1 != -1 {
331+
pos1++
332+
} else if strings.HasPrefix(s, k+"=") {
333+
pos1 = 0
334+
}
335+
}
336+
pos2 := len(s)
337+
if pos1 == -1 {
338+
pos1 = len(s)
339+
} else {
340+
pos2 = pos1 + 1
341+
for pos2 < len(s) && s[pos2-1] != '&' {
342+
pos2++
343+
}
344+
}
345+
if v != "" {
346+
sep := ""
347+
hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&'))
348+
if !hasPrefixSep {
349+
sep = "&"
350+
}
351+
s = s[:pos1] + sep + k + "=" + url.QueryEscape(v) + "&" + s[pos2:]
352+
} else {
353+
s = s[:pos1] + s[pos2:]
354+
}
355+
}
356+
if s != "" && s != "&" && s[len(s)-1] == '&' {
357+
s = s[:len(s)-1]
358+
}
359+
return QueryString(s)
360+
}
361+
296362
func panicIfDevOrTesting() {
297363
if !setting.IsProd || setting.IsInTesting {
298364
panic("legacy template functions are for backward compatibility only, do not use them in new code")

routers/web/repo/issue_list.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -504,19 +504,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
504504
if !util.SliceContainsString(types, viewType, true) {
505505
viewType = "all"
506506
}
507-
508-
var (
509-
assigneeID = ctx.FormInt64("assignee")
510-
posterID = ctx.FormInt64("poster")
511-
mentionedID int64
512-
reviewRequestedID int64
513-
reviewedID int64
514-
)
507+
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
508+
assigneeID := ctx.FormInt64("assignee")
509+
posterUsername := ctx.FormString("poster")
510+
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
511+
var mentionedID, reviewRequestedID, reviewedID int64
515512

516513
if ctx.IsSigned {
517514
switch viewType {
518515
case "created_by":
519-
posterID = ctx.Doer.ID
516+
posterUserID = ctx.Doer.ID
520517
case "mentioned":
521518
mentionedID = ctx.Doer.ID
522519
case "assigned":
@@ -564,7 +561,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
564561
ProjectID: projectID,
565562
AssigneeID: assigneeID,
566563
MentionedID: mentionedID,
567-
PosterID: posterID,
564+
PosterID: posterUserID,
568565
ReviewRequestedID: reviewRequestedID,
569566
ReviewedID: reviewedID,
570567
IsPull: isPullOption,
@@ -646,7 +643,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
646643
},
647644
RepoIDs: []int64{repo.ID},
648645
AssigneeID: assigneeID,
649-
PosterID: posterID,
646+
PosterID: posterUserID,
650647
MentionedID: mentionedID,
651648
ReviewRequestedID: reviewRequestedID,
652649
ReviewedID: reviewedID,
@@ -800,24 +797,24 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
800797
ctx.Data["IssueStats"] = issueStats
801798
ctx.Data["OpenCount"] = issueStats.OpenCount
802799
ctx.Data["ClosedCount"] = issueStats.ClosedCount
803-
linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
800+
linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%v&archived=%t"
804801
ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
805802
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
806-
milestoneID, projectID, assigneeID, posterID, archived)
803+
milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived)
807804
ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
808805
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
809-
milestoneID, projectID, assigneeID, posterID, archived)
806+
milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived)
810807
ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link,
811808
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
812-
milestoneID, projectID, assigneeID, posterID, archived)
809+
milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived)
813810
ctx.Data["SelLabelIDs"] = labelIDs
814811
ctx.Data["SelectLabels"] = selectLabels
815812
ctx.Data["ViewType"] = viewType
816813
ctx.Data["SortType"] = sortType
817814
ctx.Data["MilestoneID"] = milestoneID
818815
ctx.Data["ProjectID"] = projectID
819816
ctx.Data["AssigneeID"] = assigneeID
820-
ctx.Data["PosterID"] = posterID
817+
ctx.Data["PosterUsername"] = posterUsername
821818
ctx.Data["Keyword"] = keyword
822819
ctx.Data["IsShowClosed"] = isShowClosed
823820
switch {
@@ -838,7 +835,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
838835
pager.AddParamString("milestone", fmt.Sprint(milestoneID))
839836
pager.AddParamString("project", fmt.Sprint(projectID))
840837
pager.AddParamString("assignee", fmt.Sprint(assigneeID))
841-
pager.AddParamString("poster", fmt.Sprint(posterID))
838+
pager.AddParamString("poster", posterUsername)
842839
pager.AddParamString("archived", fmt.Sprint(archived))
843840

844841
ctx.Data["Page"] = pager

routers/web/shared/user/helper.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package user
55

66
import (
7+
"context"
78
"slices"
9+
"strconv"
810

911
"code.gitea.io/gitea/models/user"
1012
)
@@ -24,3 +26,22 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
2426
}
2527
return users
2628
}
29+
30+
// GetFilterUserIDByName tries to get the user ID from the given username.
31+
// Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list.
32+
// So it's better to make it work like GitHub: users could input username directly.
33+
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
34+
// Old usage: poster=123, new usage: poster=the-username (at the moment, non-existing username is treated as poster=0, not ideal but acceptable)
35+
func GetFilterUserIDByName(ctx context.Context, name string) int64 {
36+
if name == "" {
37+
return 0
38+
}
39+
u, err := user.GetUserByName(ctx, name)
40+
if err != nil {
41+
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
42+
return id
43+
}
44+
return 0
45+
}
46+
return u.ID
47+
}

routers/web/user/home.go

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import (
3131
"code.gitea.io/gitea/modules/markup/markdown"
3232
"code.gitea.io/gitea/modules/optional"
3333
"code.gitea.io/gitea/modules/setting"
34+
"code.gitea.io/gitea/modules/util"
3435
"code.gitea.io/gitea/routers/web/feed"
36+
"code.gitea.io/gitea/routers/web/shared/user"
3537
"code.gitea.io/gitea/services/context"
3638
feed_service "code.gitea.io/gitea/services/feed"
3739
issue_service "code.gitea.io/gitea/services/issue"
@@ -375,16 +377,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
375377
return
376378
}
377379

378-
var (
379-
viewType string
380-
sortType = ctx.FormString("sort")
381-
filterMode int
382-
)
383-
384380
// Default to recently updated, unlike repository issues list
385-
if sortType == "" {
386-
sortType = "recentupdate"
387-
}
381+
sortType := util.IfZero(ctx.FormString("sort"), "recentupdate")
388382

389383
// --------------------------------------------------------------------------------
390384
// Distinguish User from Organization.
@@ -399,7 +393,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
399393

400394
// TODO: distinguish during routing
401395

402-
viewType = ctx.FormString("type")
396+
viewType := ctx.FormString("type")
397+
var filterMode int
403398
switch viewType {
404399
case "assigned":
405400
filterMode = issues_model.FilterModeAssign
@@ -443,6 +438,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
443438
Team: team,
444439
User: ctx.Doer,
445440
}
441+
// Get filter by author id & assignee id
442+
// FIXME: this feature doesn't work at the moment, because frontend can't use a "user-remote-search" dropdown directly
443+
// the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly.
444+
// In the future, we need something like github: "author:user1" to accept usernames directly.
445+
posterUsername := ctx.FormString("poster")
446+
opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername)
447+
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
448+
opts.AssigneeID, _ = strconv.ParseInt(ctx.FormString("assignee"), 10, 64)
446449

447450
isFuzzy := ctx.FormBool("fuzzy")
448451

@@ -573,8 +576,22 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
573576
// -------------------------------
574577
// Fill stats to post to ctx.Data.
575578
// -------------------------------
576-
issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
577-
func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy },
579+
issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
580+
func(o *issue_indexer.SearchOptions) {
581+
o.IsFuzzyKeyword = isFuzzy
582+
// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
583+
// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
584+
// because the doer may create issues or be mentioned in any public repo.
585+
// So we need search issues in all public repos.
586+
o.AllPublic = ctx.Doer.ID == ctxUser.ID
587+
// TODO: to make it work with poster/assignee filter, then these IDs should be kept
588+
o.AssigneeID = nil
589+
o.PosterID = nil
590+
591+
o.MentionID = nil
592+
o.ReviewRequestedID = nil
593+
o.ReviewedID = nil
594+
},
578595
))
579596
if err != nil {
580597
ctx.ServerError("getUserIssueStats", err)
@@ -630,6 +647,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
630647
ctx.Data["IsShowClosed"] = isShowClosed
631648
ctx.Data["SelectLabels"] = selectedLabels
632649
ctx.Data["IsFuzzy"] = isFuzzy
650+
ctx.Data["SearchFilterPosterID"] = util.Iif[any](opts.PosterID != 0, opts.PosterID, nil)
651+
ctx.Data["SearchFilterAssigneeID"] = util.Iif[any](opts.AssigneeID != 0, opts.AssigneeID, nil)
633652

634653
if isShowClosed {
635654
ctx.Data["State"] = "closed"
@@ -643,7 +662,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
643662
pager.AddParamString("sort", sortType)
644663
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
645664
pager.AddParamString("labels", selectedLabels)
646-
pager.AddParamString("fuzzy", fmt.Sprintf("%v", isFuzzy))
665+
pager.AddParamString("fuzzy", fmt.Sprint(isFuzzy))
666+
pager.AddParamString("poster", posterUsername)
667+
if opts.AssigneeID != 0 {
668+
pager.AddParamString("assignee", fmt.Sprint(opts.AssigneeID))
669+
}
647670
ctx.Data["Page"] = pager
648671

649672
ctx.HTML(http.StatusOK, tplIssues)
@@ -768,27 +791,10 @@ func UsernameSubRoute(ctx *context.Context) {
768791
}
769792
}
770793

771-
func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (*issues_model.IssueStats, error) {
794+
func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
795+
ret = &issues_model.IssueStats{}
772796
doerID := ctx.Doer.ID
773797

774-
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
775-
// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
776-
// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
777-
// because the doer may create issues or be mentioned in any public repo.
778-
// So we need search issues in all public repos.
779-
o.AllPublic = doerID == ctxUser.ID
780-
o.AssigneeID = nil
781-
o.PosterID = nil
782-
o.MentionID = nil
783-
o.ReviewRequestedID = nil
784-
o.ReviewedID = nil
785-
})
786-
787-
var (
788-
err error
789-
ret = &issues_model.IssueStats{}
790-
)
791-
792798
{
793799
openClosedOpts := opts.Copy()
794800
switch filterMode {

0 commit comments

Comments
 (0)