diff options
-rw-r--r-- | modules/templates/helper.go | 66 | ||||
-rw-r--r-- | routers/web/repo/issue_list.go | 31 | ||||
-rw-r--r-- | routers/web/shared/user/helper.go | 21 | ||||
-rw-r--r-- | routers/web/user/home.go | 70 | ||||
-rw-r--r-- | templates/repo/issue/filter_list.tmpl | 59 | ||||
-rw-r--r-- | templates/repo/issue/search.tmpl | 2 | ||||
-rw-r--r-- | templates/user/dashboard/issues.tmpl | 41 | ||||
-rw-r--r-- | web_src/js/features/repo-issue-list.ts | 111 |
8 files changed, 246 insertions, 155 deletions
diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e6442fa87e..fdfb21925a 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -42,6 +42,7 @@ func NewFuncMap() template.FuncMap { "HTMLFormat": htmlutil.HTMLFormat, "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, + "QueryBuild": queryBuild, "JSEscape": jsEscapeSafe, "SanitizeHTML": SanitizeHTML, "URLJoin": util.URLJoin, @@ -293,6 +294,71 @@ func timeEstimateString(timeSec any) string { return util.TimeEstimateString(v) } +type QueryString string + +func queryBuild(a ...any) QueryString { + var s string + if len(a)%2 == 1 { + if v, ok := a[0].(string); ok { + if v == "" || (v[0] != '?' && v[0] != '&') { + panic("queryBuild: invalid argument") + } + s = v + } else if v, ok := a[0].(QueryString); ok { + s = string(v) + } else { + panic("queryBuild: invalid argument") + } + } + for i := len(a) % 2; i < len(a); i += 2 { + k, ok := a[i].(string) + if !ok { + panic("queryBuild: invalid argument") + } + var v string + if va, ok := a[i+1].(string); ok { + v = va + } else if a[i+1] != nil { + v = fmt.Sprint(a[i+1]) + } + // pos1 to pos2 is the "k=v&" part, "&" is optional + pos1 := strings.Index(s, "&"+k+"=") + if pos1 != -1 { + pos1++ + } else { + pos1 = strings.Index(s, "?"+k+"=") + if pos1 != -1 { + pos1++ + } else if strings.HasPrefix(s, k+"=") { + pos1 = 0 + } + } + pos2 := len(s) + if pos1 == -1 { + pos1 = len(s) + } else { + pos2 = pos1 + 1 + for pos2 < len(s) && s[pos2-1] != '&' { + pos2++ + } + } + if v != "" { + sep := "" + hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&')) + if !hasPrefixSep { + sep = "&" + } + s = s[:pos1] + sep + k + "=" + url.QueryEscape(v) + "&" + s[pos2:] + } else { + s = s[:pos1] + s[pos2:] + } + } + if s != "" && s != "&" && s[len(s)-1] == '&' { + s = s[:len(s)-1] + } + return QueryString(s) +} + func panicIfDevOrTesting() { if !setting.IsProd || setting.IsInTesting { panic("legacy template functions are for backward compatibility only, do not use them in new code") diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 50bb668433..2123d4a5b6 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -504,19 +504,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt if !util.SliceContainsString(types, viewType, true) { viewType = "all" } - - var ( - assigneeID = ctx.FormInt64("assignee") - posterID = ctx.FormInt64("poster") - mentionedID int64 - reviewRequestedID int64 - reviewedID int64 - ) + // TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly + assigneeID := ctx.FormInt64("assignee") + posterUsername := ctx.FormString("poster") + posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername) + var mentionedID, reviewRequestedID, reviewedID int64 if ctx.IsSigned { switch viewType { case "created_by": - posterID = ctx.Doer.ID + posterUserID = ctx.Doer.ID case "mentioned": mentionedID = ctx.Doer.ID case "assigned": @@ -564,7 +561,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ProjectID: projectID, AssigneeID: assigneeID, MentionedID: mentionedID, - PosterID: posterID, + PosterID: posterUserID, ReviewRequestedID: reviewRequestedID, ReviewedID: reviewedID, IsPull: isPullOption, @@ -646,7 +643,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt }, RepoIDs: []int64{repo.ID}, AssigneeID: assigneeID, - PosterID: posterID, + PosterID: posterUserID, MentionedID: mentionedID, ReviewRequestedID: reviewRequestedID, ReviewedID: reviewedID, @@ -800,16 +797,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["IssueStats"] = issueStats ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["ClosedCount"] = issueStats.ClosedCount - linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t" + linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%v&archived=%t" ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link, url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, posterID, archived) + milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived) ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link, url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, posterID, archived) + milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived) ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link, url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, posterID, archived) + milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived) ctx.Data["SelLabelIDs"] = labelIDs ctx.Data["SelectLabels"] = selectLabels ctx.Data["ViewType"] = viewType @@ -817,7 +814,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["MilestoneID"] = milestoneID ctx.Data["ProjectID"] = projectID ctx.Data["AssigneeID"] = assigneeID - ctx.Data["PosterID"] = posterID + ctx.Data["PosterUsername"] = posterUsername ctx.Data["Keyword"] = keyword ctx.Data["IsShowClosed"] = isShowClosed switch { @@ -838,7 +835,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt pager.AddParamString("milestone", fmt.Sprint(milestoneID)) pager.AddParamString("project", fmt.Sprint(projectID)) pager.AddParamString("assignee", fmt.Sprint(assigneeID)) - pager.AddParamString("poster", fmt.Sprint(posterID)) + pager.AddParamString("poster", posterUsername) pager.AddParamString("archived", fmt.Sprint(archived)) ctx.Data["Page"] = pager diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index dfd65420c1..7268767e0a 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -4,7 +4,9 @@ package user import ( + "context" "slices" + "strconv" "code.gitea.io/gitea/models/user" ) @@ -24,3 +26,22 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { } return users } + +// GetFilterUserIDByName tries to get the user ID from the given username. +// 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. +// So it's better to make it work like GitHub: users could input username directly. +// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed. +// Old usage: poster=123, new usage: poster=the-username (at the moment, non-existing username is treated as poster=0, not ideal but acceptable) +func GetFilterUserIDByName(ctx context.Context, name string) int64 { + if name == "" { + return 0 + } + u, err := user.GetUserByName(ctx, name) + if err != nil { + if id, err := strconv.ParseInt(name, 10, 64); err == nil { + return id + } + return 0 + } + return u.ID +} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 0cf932ac03..5a0d46869f 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -31,7 +31,9 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" feed_service "code.gitea.io/gitea/services/feed" issue_service "code.gitea.io/gitea/services/issue" @@ -375,16 +377,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { return } - var ( - viewType string - sortType = ctx.FormString("sort") - filterMode int - ) - // Default to recently updated, unlike repository issues list - if sortType == "" { - sortType = "recentupdate" - } + sortType := util.IfZero(ctx.FormString("sort"), "recentupdate") // -------------------------------------------------------------------------------- // Distinguish User from Organization. @@ -399,7 +393,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // TODO: distinguish during routing - viewType = ctx.FormString("type") + viewType := ctx.FormString("type") + var filterMode int switch viewType { case "assigned": filterMode = issues_model.FilterModeAssign @@ -443,6 +438,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { Team: team, User: ctx.Doer, } + // Get filter by author id & assignee id + // FIXME: this feature doesn't work at the moment, because frontend can't use a "user-remote-search" dropdown directly + // the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly. + // In the future, we need something like github: "author:user1" to accept usernames directly. + posterUsername := ctx.FormString("poster") + opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername) + // TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly + opts.AssigneeID, _ = strconv.ParseInt(ctx.FormString("assignee"), 10, 64) isFuzzy := ctx.FormBool("fuzzy") @@ -573,8 +576,22 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( - func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy }, + issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( + func(o *issue_indexer.SearchOptions) { + o.IsFuzzyKeyword = isFuzzy + // If the doer is the same as the context user, which means the doer is viewing his own dashboard, + // it's not enough to show the repos that the doer owns or has been explicitly granted access to, + // because the doer may create issues or be mentioned in any public repo. + // So we need search issues in all public repos. + o.AllPublic = ctx.Doer.ID == ctxUser.ID + // TODO: to make it work with poster/assignee filter, then these IDs should be kept + o.AssigneeID = nil + o.PosterID = nil + + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }, )) if err != nil { ctx.ServerError("getUserIssueStats", err) @@ -630,6 +647,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["SelectLabels"] = selectedLabels ctx.Data["IsFuzzy"] = isFuzzy + ctx.Data["SearchFilterPosterID"] = util.Iif[any](opts.PosterID != 0, opts.PosterID, nil) + ctx.Data["SearchFilterAssigneeID"] = util.Iif[any](opts.AssigneeID != 0, opts.AssigneeID, nil) if isShowClosed { ctx.Data["State"] = "closed" @@ -643,7 +662,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { pager.AddParamString("sort", sortType) pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) pager.AddParamString("labels", selectedLabels) - pager.AddParamString("fuzzy", fmt.Sprintf("%v", isFuzzy)) + pager.AddParamString("fuzzy", fmt.Sprint(isFuzzy)) + pager.AddParamString("poster", posterUsername) + if opts.AssigneeID != 0 { + pager.AddParamString("assignee", fmt.Sprint(opts.AssigneeID)) + } ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplIssues) @@ -768,27 +791,10 @@ func UsernameSubRoute(ctx *context.Context) { } } -func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (*issues_model.IssueStats, error) { +func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { + ret = &issues_model.IssueStats{} doerID := ctx.Doer.ID - opts = opts.Copy(func(o *issue_indexer.SearchOptions) { - // If the doer is the same as the context user, which means the doer is viewing his own dashboard, - // it's not enough to show the repos that the doer owns or has been explicitly granted access to, - // because the doer may create issues or be mentioned in any public repo. - // So we need search issues in all public repos. - o.AllPublic = doerID == ctxUser.ID - o.AssigneeID = nil - o.PosterID = nil - o.MentionID = nil - o.ReviewRequestedID = nil - o.ReviewedID = nil - }) - - var ( - err error - ret = &issues_model.IssueStats{} - ) - { openClosedOpts := opts.Copy() switch filterMode { diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index d48af5b150..7335c949f4 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -1,3 +1,4 @@ +{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived" (Iif $.ShowArchivedLabels NIL)}} <!-- Label --> <div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter"> <span class="text"> @@ -23,8 +24,8 @@ </div> <span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span> <div class="divider"></div> - <a 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 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> + <a class="{{if .AllLabels}}active selected {{end}}item" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a> + <a class="{{if .NoLabel}}active selected {{end}}item" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} @@ -32,7 +33,7 @@ <div class="divider"></div> {{end}} {{$previousExclusiveScope = $exclusiveScope}} - <a 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}}"> + <a class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="{{QueryBuild $queryLink "labels" .QueryString}}" data-label-id="{{.ID}}"> {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} @@ -62,13 +63,13 @@ <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}"> </div> <div class="divider"></div> - <a class="{{if not $.MilestoneID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a> - <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a> + <a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" 0}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a> + <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a> {{if .OpenMilestones}} <div class="divider"></div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div> {{range .OpenMilestones}} - <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}"> {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} </a> @@ -78,7 +79,7 @@ <div class="divider"></div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div> {{range .ClosedMilestones}} - <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" .ID}}"> {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} </a> @@ -99,15 +100,15 @@ <i class="icon">{{svg "octicon-search" 16}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}"> </div> - <a class="{{if not .ProjectID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a> - <a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a> + <a class="{{if not .ProjectID}}active selected {{end}}item" href="{{QueryBuild $queryLink "project" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a> + <a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="{{QueryBuild $queryLink "project" -1}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a> {{if .OpenProjects}} <div class="divider"></div> <div class="header"> {{ctx.Locale.Tr "repo.issues.new.open_projects"}} </div> {{range .OpenProjects}} - <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="{{QueryBuild $queryLink "project" .ID}}"> {{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span> </a> {{end}} @@ -118,7 +119,7 @@ {{ctx.Locale.Tr "repo.issues.new.closed_projects"}} </div> {{range .ClosedProjects}} - <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="{{QueryBuild $queryLink "project" .ID}}"> {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </a> {{end}} @@ -130,7 +131,7 @@ <div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.author_search_tooltip"}}" data-search-url="{{if .Milestone}}{{$.RepoLink}}/issues/posters{{else}}{{$.Link}}/posters{{end}}" data-selected-user-id="{{$.PosterID}}" - data-action-jump-url="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}" + data-action-jump-url="{{QueryBuild $queryLink "poster" NIL}}&poster={username}" > <span class="text"> {{ctx.Locale.Tr "repo.issues.filter_poster"}} @@ -156,11 +157,11 @@ <i class="icon">{{svg "octicon-search" 16}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}"> </div> - <a class="{{if not .AssigneeID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a> - <a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a> + <a class="{{if not .AssigneeID}}active selected {{end}}item" href="{{QueryBuild $queryLink "assignee" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a> + <a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="{{QueryBuild $queryLink "assignee" -1}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a> <div class="divider"></div> {{range .Assignees}} - <a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item tw-flex" href="{{QueryBuild $queryLink "assignee" .ID}}"> {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} </a> {{end}} @@ -175,14 +176,14 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if eq .ViewType "all"}}active {{end}}item" href="?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a> - <a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a> - <a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a> + <a class="{{if eq .ViewType "all"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "all"}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a> + <a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "assigned"}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a> + <a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "created_by"}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a> {{if .PageIsPullList}} - <a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a> - <a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a> + <a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "review_requested"}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a> + <a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a> {{end}} - <a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a> + <a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "mentioned"}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a> </div> </div> {{end}} @@ -194,13 +195,13 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> - <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> - <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> - <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> + <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "latest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "oldest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "recentupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "leastupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "mostcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> + <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> + <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> + <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> </div> </div> diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index 769387b51c..1ab0dc74f3 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -7,7 +7,7 @@ <input type="hidden" name="milestone" value="{{$.MilestoneID}}"> <input type="hidden" name="project" value="{{$.ProjectID}}"> <input type="hidden" name="assignee" value="{{$.AssigneeID}}"> - <input type="hidden" name="poster" value="{{$.PosterID}}"> + <input type="hidden" name="poster" value="{{$.PosterUsername}}"> {{end}} {{template "shared/search/input" dict "Value" .Keyword}} {{if .PageIsIssueList}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index b47a21e87c..b9d63818fe 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -4,45 +4,48 @@ <div class="ui container"> {{template "base/alert" .}} <div class="flex-container"> + {{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "fuzzy" $.IsFuzzy}} <div class="flex-container-nav"> <div class="ui secondary vertical filter menu tw-bg-transparent"> - <a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> + <a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}"> {{ctx.Locale.Tr "home.issues.in_your_repos"}} <strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong> </a> - <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> + <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}"> {{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}} <strong>{{CountFmt .IssueStats.AssignCount}}</strong> </a> - <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> + <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "created_by"}}"> {{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}} <strong>{{CountFmt .IssueStats.CreateCount}}</strong> </a> {{if .PageIsPulls}} - <a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> + <a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "review_requested"}}"> {{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}} <strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong> </a> - <a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> + <a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}"> {{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}} <strong>{{CountFmt .IssueStats.ReviewedCount}}</strong> </a> {{end}} - <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> + <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}"> {{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}} <strong>{{CountFmt .IssueStats.MentionCount}}</strong> </a> </div> </div> + + {{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.SearchFilterPosterUsername "assignee" $.SearchFilterAssigneeID}} <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}}&fuzzy={{.IsFuzzy}}"> - {{svg "octicon-issue-opened" 16 "tw-mr-2"}} + <div class="small-menu-items ui compact tiny menu list-header-toggle flex-items-block"> + <a class="item{{if not .IsShowClosed}} active{{end}}" href="{{QueryBuild $queryLink "state" "open"}}"> + {{svg "octicon-issue-opened"}} {{ctx.Locale.PrettyNumber .IssueStats.OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} </a> - <a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> - {{svg "octicon-issue-closed" 16 "tw-mr-2"}} + <a class="item{{if .IsShowClosed}} active{{end}}" href="{{QueryBuild $queryLink "state" "closed"}}"> + {{svg "octicon-issue-closed"}} {{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> </div> @@ -61,14 +64,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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{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}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "recentupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if eq .SortType "latest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "latest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "oldest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "mostcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> + <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> + <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> + <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> </div> </div> </div> diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index a7185e5f99..48e22ba3c9 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -1,17 +1,17 @@ -import $ from 'jquery'; import {updateIssuesMeta} from './repo-common.ts'; -import {toggleElem, hideElem, isElemHidden} from '../utils/dom.ts'; +import {toggleElem, hideElem, isElemHidden, queryElems} from '../utils/dom.ts'; import {htmlEscape} from 'escape-goat'; import {confirmModal} from './comp/ConfirmModal.ts'; import {showErrorToast} from '../modules/toast.ts'; import {createSortable} from '../modules/sortable.ts'; import {DELETE, POST} from '../modules/fetch.ts'; import {parseDom} from '../utils.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; function initRepoIssueListCheckboxes() { - const issueSelectAll = document.querySelector('.issue-checkbox-all'); + const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all'); if (!issueSelectAll) return; // logged out state - const issueCheckboxes = document.querySelectorAll('.issue-checkbox'); + const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.issue-checkbox'); const syncIssueSelectionState = () => { const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked); @@ -29,8 +29,8 @@ function initRepoIssueListCheckboxes() { issueSelectAll.indeterminate = false; } // if any issue is selected, show the action panel, otherwise show the filter panel - toggleElem($('#issue-filters'), !anyChecked); - toggleElem($('#issue-actions'), anyChecked); + toggleElem('#issue-filters', !anyChecked); + toggleElem('#issue-actions', anyChecked); // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel const panels = document.querySelectorAll('#issue-filters, #issue-actions'); const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el)); @@ -49,56 +49,55 @@ function initRepoIssueListCheckboxes() { syncIssueSelectionState(); }); - $('.issue-action').on('click', async function (e) { - e.preventDefault(); + queryElems(document, '.issue-action', (el) => el.addEventListener('click', + async (e: MouseEvent) => { + e.preventDefault(); - const url = this.getAttribute('data-url'); - let action = this.getAttribute('data-action'); - let elementId = this.getAttribute('data-element-id'); - let issueIDs = []; - for (const el of document.querySelectorAll('.issue-checkbox:checked')) { - issueIDs.push(el.getAttribute('data-issue-id')); - } - issueIDs = issueIDs.join(','); - if (!issueIDs) return; + const url = el.getAttribute('data-url'); + let action = el.getAttribute('data-action'); + let elementId = el.getAttribute('data-element-id'); + const issueIDList: string[] = []; + for (const el of document.querySelectorAll('.issue-checkbox:checked')) { + issueIDList.push(el.getAttribute('data-issue-id')); + } + const issueIDs = issueIDList.join(','); + if (!issueIDs) return; - // for assignee - if (elementId === '0' && url.endsWith('/assignee')) { - elementId = ''; - action = 'clear'; - } + // for assignee + if (elementId === '0' && url.endsWith('/assignee')) { + elementId = ''; + action = 'clear'; + } - // for toggle - if (action === 'toggle' && e.altKey) { - action = 'toggle-alt'; - } + // for toggle + if (action === 'toggle' && e.altKey) { + action = 'toggle-alt'; + } - // for delete - if (action === 'delete') { - const confirmText = e.target.getAttribute('data-action-delete-confirm'); - if (!await confirmModal({content: confirmText, confirmButtonColor: 'red'})) { - return; + // for delete + if (action === 'delete') { + const confirmText = el.getAttribute('data-action-delete-confirm'); + if (!await confirmModal({content: confirmText, confirmButtonColor: 'red'})) { + return; + } } - } - try { - await updateIssuesMeta(url, action, issueIDs, elementId); - window.location.reload(); - } catch (err) { - showErrorToast(err.responseJSON?.error ?? err.message); - } - }); + try { + await updateIssuesMeta(url, action, issueIDs, elementId); + window.location.reload(); + } catch (err) { + showErrorToast(err.responseJSON?.error ?? err.message); + } + }, + )); } -function initRepoIssueListAuthorDropdown() { - const $searchDropdown = $('.user-remote-search'); - if (!$searchDropdown.length) return; - - let searchUrl = $searchDropdown[0].getAttribute('data-search-url'); - const actionJumpUrl = $searchDropdown[0].getAttribute('data-action-jump-url'); - const selectedUserId = $searchDropdown[0].getAttribute('data-selected-user-id'); +function initDropdownUserRemoteSearch(el: Element) { + let searchUrl = el.getAttribute('data-search-url'); + const actionJumpUrl = el.getAttribute('data-action-jump-url'); + const selectedUserId = el.getAttribute('data-selected-user-id'); if (!searchUrl.includes('?')) searchUrl += '?'; - + const $searchDropdown = fomanticQuery(el); $searchDropdown.dropdown('setting', { fullTextSearch: true, selectOnKeydown: false, @@ -111,14 +110,14 @@ function initRepoIssueListAuthorDropdown() { for (const item of resp.results) { let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`; - processedResults.push({value: item.user_id, name: html}); + processedResults.push({value: item.username, name: html}); } resp.results = processedResults; return resp; }, }, action: (_text, value) => { - window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value)); + window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); }, onShow: () => { $searchDropdown.dropdown('filter', ' '); // trigger a search on first show @@ -160,7 +159,7 @@ function initRepoIssueListAuthorDropdown() { function initPinRemoveButton() { for (const button of document.querySelectorAll('.issue-card-unpin')) { button.addEventListener('click', async (event) => { - const el = event.currentTarget; + const el = event.currentTarget as HTMLElement; const id = Number(el.getAttribute('data-issue-id')); // Send the unpin request @@ -205,10 +204,8 @@ async function initIssuePinSort() { } function initArchivedLabelFilter() { - const archivedLabelEl = document.querySelector('#archived-filter-checkbox'); - if (!archivedLabelEl) { - return; - } + const archivedLabelEl = document.querySelector<HTMLInputElement>('#archived-filter-checkbox'); + if (!archivedLabelEl) return; const url = new URL(window.location.href); const archivedLabels = document.querySelectorAll('[data-is-archived]'); @@ -219,7 +216,7 @@ function initArchivedLabelFilter() { } const selectedLabels = (url.searchParams.get('labels') || '') .split(',') - .map((id) => id < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve + .map((id) => parseInt(id) < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve const archivedElToggle = () => { for (const label of archivedLabels) { @@ -241,9 +238,9 @@ function initArchivedLabelFilter() { } export function initRepoIssueList() { - if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list').length) return; + if (!document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) return; initRepoIssueListCheckboxes(); - initRepoIssueListAuthorDropdown(); + queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el)); initIssuePinSort(); initArchivedLabelFilter(); } |