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:
This commit is contained in:
wxiaoguang 2024-12-08 20:44:17 +08:00 committed by GitHub
parent 9d08d3fbf5
commit 23471e1333
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 249 additions and 158 deletions

View File

@ -42,6 +42,7 @@ func NewFuncMap() template.FuncMap {
"HTMLFormat": htmlutil.HTMLFormat, "HTMLFormat": htmlutil.HTMLFormat,
"HTMLEscape": htmlEscape, "HTMLEscape": htmlEscape,
"QueryEscape": queryEscape, "QueryEscape": queryEscape,
"QueryBuild": queryBuild,
"JSEscape": jsEscapeSafe, "JSEscape": jsEscapeSafe,
"SanitizeHTML": SanitizeHTML, "SanitizeHTML": SanitizeHTML,
"URLJoin": util.URLJoin, "URLJoin": util.URLJoin,
@ -293,6 +294,71 @@ func timeEstimateString(timeSec any) string {
return util.TimeEstimateString(v) 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() { func panicIfDevOrTesting() {
if !setting.IsProd || setting.IsInTesting { if !setting.IsProd || setting.IsInTesting {
panic("legacy template functions are for backward compatibility only, do not use them in new code") panic("legacy template functions are for backward compatibility only, do not use them in new code")

View File

@ -504,19 +504,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
if !util.SliceContainsString(types, viewType, true) { if !util.SliceContainsString(types, viewType, true) {
viewType = "all" viewType = "all"
} }
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
var ( assigneeID := ctx.FormInt64("assignee")
assigneeID = ctx.FormInt64("assignee") posterUsername := ctx.FormString("poster")
posterID = ctx.FormInt64("poster") posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
mentionedID int64 var mentionedID, reviewRequestedID, reviewedID int64
reviewRequestedID int64
reviewedID int64
)
if ctx.IsSigned { if ctx.IsSigned {
switch viewType { switch viewType {
case "created_by": case "created_by":
posterID = ctx.Doer.ID posterUserID = ctx.Doer.ID
case "mentioned": case "mentioned":
mentionedID = ctx.Doer.ID mentionedID = ctx.Doer.ID
case "assigned": case "assigned":
@ -564,7 +561,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ProjectID: projectID, ProjectID: projectID,
AssigneeID: assigneeID, AssigneeID: assigneeID,
MentionedID: mentionedID, MentionedID: mentionedID,
PosterID: posterID, PosterID: posterUserID,
ReviewRequestedID: reviewRequestedID, ReviewRequestedID: reviewRequestedID,
ReviewedID: reviewedID, ReviewedID: reviewedID,
IsPull: isPullOption, IsPull: isPullOption,
@ -646,7 +643,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
}, },
RepoIDs: []int64{repo.ID}, RepoIDs: []int64{repo.ID},
AssigneeID: assigneeID, AssigneeID: assigneeID,
PosterID: posterID, PosterID: posterUserID,
MentionedID: mentionedID, MentionedID: mentionedID,
ReviewRequestedID: reviewRequestedID, ReviewRequestedID: reviewRequestedID,
ReviewedID: reviewedID, ReviewedID: reviewedID,
@ -800,16 +797,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["IssueStats"] = issueStats ctx.Data["IssueStats"] = issueStats
ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount 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, ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels), 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, ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels), 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, ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels), 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["SelLabelIDs"] = labelIDs
ctx.Data["SelectLabels"] = selectLabels ctx.Data["SelectLabels"] = selectLabels
ctx.Data["ViewType"] = viewType ctx.Data["ViewType"] = viewType
@ -817,7 +814,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["MilestoneID"] = milestoneID ctx.Data["MilestoneID"] = milestoneID
ctx.Data["ProjectID"] = projectID ctx.Data["ProjectID"] = projectID
ctx.Data["AssigneeID"] = assigneeID ctx.Data["AssigneeID"] = assigneeID
ctx.Data["PosterID"] = posterID ctx.Data["PosterUsername"] = posterUsername
ctx.Data["Keyword"] = keyword ctx.Data["Keyword"] = keyword
ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["IsShowClosed"] = isShowClosed
switch { switch {
@ -838,7 +835,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
pager.AddParamString("milestone", fmt.Sprint(milestoneID)) pager.AddParamString("milestone", fmt.Sprint(milestoneID))
pager.AddParamString("project", fmt.Sprint(projectID)) pager.AddParamString("project", fmt.Sprint(projectID))
pager.AddParamString("assignee", fmt.Sprint(assigneeID)) pager.AddParamString("assignee", fmt.Sprint(assigneeID))
pager.AddParamString("poster", fmt.Sprint(posterID)) pager.AddParamString("poster", posterUsername)
pager.AddParamString("archived", fmt.Sprint(archived)) pager.AddParamString("archived", fmt.Sprint(archived))
ctx.Data["Page"] = pager ctx.Data["Page"] = pager

View File

@ -4,7 +4,9 @@
package user package user
import ( import (
"context"
"slices" "slices"
"strconv"
"code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/user"
) )
@ -24,3 +26,22 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
} }
return users 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
}

View File

@ -31,7 +31,9 @@ import (
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "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/feed"
"code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
feed_service "code.gitea.io/gitea/services/feed" feed_service "code.gitea.io/gitea/services/feed"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
@ -375,16 +377,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
return return
} }
var (
viewType string
sortType = ctx.FormString("sort")
filterMode int
)
// Default to recently updated, unlike repository issues list // Default to recently updated, unlike repository issues list
if sortType == "" { sortType := util.IfZero(ctx.FormString("sort"), "recentupdate")
sortType = "recentupdate"
}
// -------------------------------------------------------------------------------- // --------------------------------------------------------------------------------
// Distinguish User from Organization. // Distinguish User from Organization.
@ -399,7 +393,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// TODO: distinguish during routing // TODO: distinguish during routing
viewType = ctx.FormString("type") viewType := ctx.FormString("type")
var filterMode int
switch viewType { switch viewType {
case "assigned": case "assigned":
filterMode = issues_model.FilterModeAssign filterMode = issues_model.FilterModeAssign
@ -443,6 +438,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
Team: team, Team: team,
User: ctx.Doer, 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") isFuzzy := ctx.FormBool("fuzzy")
@ -573,8 +576,22 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// ------------------------------- // -------------------------------
// Fill stats to post to ctx.Data. // Fill stats to post to ctx.Data.
// ------------------------------- // -------------------------------
issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy }, 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 { if err != nil {
ctx.ServerError("getUserIssueStats", err) ctx.ServerError("getUserIssueStats", err)
@ -630,6 +647,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["SelectLabels"] = selectedLabels ctx.Data["SelectLabels"] = selectedLabels
ctx.Data["IsFuzzy"] = isFuzzy 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 { if isShowClosed {
ctx.Data["State"] = "closed" ctx.Data["State"] = "closed"
@ -643,7 +662,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
pager.AddParamString("sort", sortType) pager.AddParamString("sort", sortType)
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
pager.AddParamString("labels", selectedLabels) 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.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplIssues) 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 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() openClosedOpts := opts.Copy()
switch filterMode { switch filterMode {

View File

@ -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 --> <!-- Label -->
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter"> <div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter">
<span class="text"> <span class="text">
@ -23,8 +24,8 @@
</div> </div>
<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span> <span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
<div class="divider"></div> <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 .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="?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 .NoLabel}}active selected {{end}}item" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
{{$previousExclusiveScope := "_no_scope"}} {{$previousExclusiveScope := "_no_scope"}}
{{range .Labels}} {{range .Labels}}
{{$exclusiveScope := .ExclusiveScope}} {{$exclusiveScope := .ExclusiveScope}}
@ -32,7 +33,7 @@
<div class="divider"></div> <div class="divider"></div>
{{end}} {{end}}
{{$previousExclusiveScope = $exclusiveScope}} {{$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}} {{if .IsExcluded}}
{{svg "octicon-circle-slash"}} {{svg "octicon-circle-slash"}}
{{else if .IsSelected}} {{else if .IsSelected}}
@ -62,13 +63,13 @@
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}"> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
</div> </div>
<div class="divider"></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 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="?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 $.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}} {{if .OpenMilestones}}
<div class="divider"></div> <div class="divider"></div>
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
{{range .OpenMilestones}} {{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"}} {{svg "octicon-milestone" 16 "mr-2"}}
{{.Name}} {{.Name}}
</a> </a>
@ -78,7 +79,7 @@
<div class="divider"></div> <div class="divider"></div>
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
{{range .ClosedMilestones}} {{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"}} {{svg "octicon-milestone" 16 "mr-2"}}
{{.Name}} {{.Name}}
</a> </a>
@ -99,15 +100,15 @@
<i class="icon">{{svg "octicon-search" 16}}</i> <i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}"> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}">
</div> </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 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="?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 eq .ProjectID -1}}active selected {{end}}item" href="{{QueryBuild $queryLink "project" -1}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
{{if .OpenProjects}} {{if .OpenProjects}}
<div class="divider"></div> <div class="divider"></div>
<div class="header"> <div class="header">
{{ctx.Locale.Tr "repo.issues.new.open_projects"}} {{ctx.Locale.Tr "repo.issues.new.open_projects"}}
</div> </div>
{{range .OpenProjects}} {{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> {{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
</a> </a>
{{end}} {{end}}
@ -118,7 +119,7 @@
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}} {{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
</div> </div>
{{range .ClosedProjects}} {{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}} {{svg .IconName 18 "tw-mr-2"}}{{.Title}}
</a> </a>
{{end}} {{end}}
@ -130,7 +131,7 @@
<div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.author_search_tooltip"}}" <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-search-url="{{if .Milestone}}{{$.RepoLink}}/issues/posters{{else}}{{$.Link}}/posters{{end}}"
data-selected-user-id="{{$.PosterID}}" 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"> <span class="text">
{{ctx.Locale.Tr "repo.issues.filter_poster"}} {{ctx.Locale.Tr "repo.issues.filter_poster"}}
@ -156,11 +157,11 @@
<i class="icon">{{svg "octicon-search" 16}}</i> <i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}"> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}">
</div> </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 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="?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 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> <div class="divider"></div>
{{range .Assignees}} {{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" .}} {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
</a> </a>
{{end}} {{end}}
@ -175,14 +176,14 @@
</span> </span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <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 "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="?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 "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="?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 "created_by"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "created_by"}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a>
{{if .PageIsPullList}} {{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 "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="?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 "reviewed_by"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a>
{{end}} {{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>
</div> </div>
{{end}} {{end}}
@ -194,13 +195,13 @@
</span> </span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <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 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="?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 "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="?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 "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="?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 "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="?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 "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="?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 "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="?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 "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="?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 eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@
<input type="hidden" name="milestone" value="{{$.MilestoneID}}"> <input type="hidden" name="milestone" value="{{$.MilestoneID}}">
<input type="hidden" name="project" value="{{$.ProjectID}}"> <input type="hidden" name="project" value="{{$.ProjectID}}">
<input type="hidden" name="assignee" value="{{$.AssigneeID}}"> <input type="hidden" name="assignee" value="{{$.AssigneeID}}">
<input type="hidden" name="poster" value="{{$.PosterID}}"> <input type="hidden" name="poster" value="{{$.PosterUsername}}">
{{end}} {{end}}
{{template "shared/search/input" dict "Value" .Keyword}} {{template "shared/search/input" dict "Value" .Keyword}}
{{if .PageIsIssueList}} {{if .PageIsIssueList}}

View File

@ -4,45 +4,48 @@
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<div class="flex-container"> <div class="flex-container">
{{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "fuzzy" $.IsFuzzy}}
<div class="flex-container-nav"> <div class="flex-container-nav">
<div class="ui secondary vertical filter menu tw-bg-transparent"> <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"}} {{ctx.Locale.Tr "home.issues.in_your_repos"}}
<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong> <strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
</a> </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"}} {{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
<strong>{{CountFmt .IssueStats.AssignCount}}</strong> <strong>{{CountFmt .IssueStats.AssignCount}}</strong>
</a> </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"}} {{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
<strong>{{CountFmt .IssueStats.CreateCount}}</strong> <strong>{{CountFmt .IssueStats.CreateCount}}</strong>
</a> </a>
{{if .PageIsPulls}} {{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"}} {{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong> <strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
</a> </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"}} {{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong> <strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
</a> </a>
{{end}} {{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"}} {{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
<strong>{{CountFmt .IssueStats.MentionCount}}</strong> <strong>{{CountFmt .IssueStats.MentionCount}}</strong>
</a> </a>
</div> </div>
</div> </div>
{{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.SearchFilterPosterUsername "assignee" $.SearchFilterAssigneeID}}
<div class="flex-container-main content"> <div class="flex-container-main content">
<div class="list-header"> <div class="list-header">
<div class="small-menu-items ui compact tiny menu list-header-toggle"> <div class="small-menu-items ui compact tiny menu list-header-toggle flex-items-block">
<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> <a class="item{{if not .IsShowClosed}} active{{end}}" href="{{QueryBuild $queryLink "state" "open"}}">
{{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{svg "octicon-issue-opened"}}
{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}} {{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
</a> </a>
<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}"> <a class="item{{if .IsShowClosed}} active{{end}}" href="{{QueryBuild $queryLink "state" "closed"}}">
{{svg "octicon-issue-closed" 16 "tw-mr-2"}} {{svg "octicon-issue-closed"}}
{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}} {{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
</a> </a>
</div> </div>
@ -61,14 +64,14 @@
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</span> </span>
<div class="menu"> <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 "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="?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</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 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 "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="?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</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="?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</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="?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</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="?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</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="?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}&fuzzy={{.IsFuzzy}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</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> </div>
</div> </div>

View File

@ -1,17 +1,17 @@
import $ from 'jquery';
import {updateIssuesMeta} from './repo-common.ts'; 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 {htmlEscape} from 'escape-goat';
import {confirmModal} from './comp/ConfirmModal.ts'; import {confirmModal} from './comp/ConfirmModal.ts';
import {showErrorToast} from '../modules/toast.ts'; import {showErrorToast} from '../modules/toast.ts';
import {createSortable} from '../modules/sortable.ts'; import {createSortable} from '../modules/sortable.ts';
import {DELETE, POST} from '../modules/fetch.ts'; import {DELETE, POST} from '../modules/fetch.ts';
import {parseDom} from '../utils.ts'; import {parseDom} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
function initRepoIssueListCheckboxes() { function initRepoIssueListCheckboxes() {
const issueSelectAll = document.querySelector('.issue-checkbox-all'); const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all');
if (!issueSelectAll) return; // logged out state if (!issueSelectAll) return; // logged out state
const issueCheckboxes = document.querySelectorAll('.issue-checkbox'); const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.issue-checkbox');
const syncIssueSelectionState = () => { const syncIssueSelectionState = () => {
const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked); const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
@ -29,8 +29,8 @@ function initRepoIssueListCheckboxes() {
issueSelectAll.indeterminate = false; issueSelectAll.indeterminate = false;
} }
// if any issue is selected, show the action panel, otherwise show the filter panel // if any issue is selected, show the action panel, otherwise show the filter panel
toggleElem($('#issue-filters'), !anyChecked); toggleElem('#issue-filters', !anyChecked);
toggleElem($('#issue-actions'), anyChecked); toggleElem('#issue-actions', anyChecked);
// there are two panels but only one select-all checkbox, so move the checkbox to the visible panel // 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 panels = document.querySelectorAll('#issue-filters, #issue-actions');
const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el)); const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
@ -49,56 +49,55 @@ function initRepoIssueListCheckboxes() {
syncIssueSelectionState(); syncIssueSelectionState();
}); });
$('.issue-action').on('click', async function (e) { queryElems(document, '.issue-action', (el) => el.addEventListener('click',
e.preventDefault(); async (e: MouseEvent) => {
e.preventDefault();
const url = this.getAttribute('data-url'); const url = el.getAttribute('data-url');
let action = this.getAttribute('data-action'); let action = el.getAttribute('data-action');
let elementId = this.getAttribute('data-element-id'); let elementId = el.getAttribute('data-element-id');
let issueIDs = []; const issueIDList: string[] = [];
for (const el of document.querySelectorAll('.issue-checkbox:checked')) { for (const el of document.querySelectorAll('.issue-checkbox:checked')) {
issueIDs.push(el.getAttribute('data-issue-id')); issueIDList.push(el.getAttribute('data-issue-id'));
}
issueIDs = issueIDs.join(',');
if (!issueIDs) return;
// for assignee
if (elementId === '0' && url.endsWith('/assignee')) {
elementId = '';
action = 'clear';
}
// 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;
} }
} const issueIDs = issueIDList.join(',');
if (!issueIDs) return;
try { // for assignee
await updateIssuesMeta(url, action, issueIDs, elementId); if (elementId === '0' && url.endsWith('/assignee')) {
window.location.reload(); elementId = '';
} catch (err) { action = 'clear';
showErrorToast(err.responseJSON?.error ?? err.message); }
}
}); // for toggle
if (action === 'toggle' && e.altKey) {
action = 'toggle-alt';
}
// 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);
}
},
));
} }
function initRepoIssueListAuthorDropdown() { function initDropdownUserRemoteSearch(el: Element) {
const $searchDropdown = $('.user-remote-search'); let searchUrl = el.getAttribute('data-search-url');
if (!$searchDropdown.length) return; const actionJumpUrl = el.getAttribute('data-action-jump-url');
const selectedUserId = el.getAttribute('data-selected-user-id');
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');
if (!searchUrl.includes('?')) searchUrl += '?'; if (!searchUrl.includes('?')) searchUrl += '?';
const $searchDropdown = fomanticQuery(el);
$searchDropdown.dropdown('setting', { $searchDropdown.dropdown('setting', {
fullTextSearch: true, fullTextSearch: true,
selectOnKeydown: false, selectOnKeydown: false,
@ -111,14 +110,14 @@ function initRepoIssueListAuthorDropdown() {
for (const item of resp.results) { 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>`; 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>`; 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; resp.results = processedResults;
return resp; return resp;
}, },
}, },
action: (_text, value) => { action: (_text, value) => {
window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value)); window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value));
}, },
onShow: () => { onShow: () => {
$searchDropdown.dropdown('filter', ' '); // trigger a search on first show $searchDropdown.dropdown('filter', ' '); // trigger a search on first show
@ -160,7 +159,7 @@ function initRepoIssueListAuthorDropdown() {
function initPinRemoveButton() { function initPinRemoveButton() {
for (const button of document.querySelectorAll('.issue-card-unpin')) { for (const button of document.querySelectorAll('.issue-card-unpin')) {
button.addEventListener('click', async (event) => { button.addEventListener('click', async (event) => {
const el = event.currentTarget; const el = event.currentTarget as HTMLElement;
const id = Number(el.getAttribute('data-issue-id')); const id = Number(el.getAttribute('data-issue-id'));
// Send the unpin request // Send the unpin request
@ -205,10 +204,8 @@ async function initIssuePinSort() {
} }
function initArchivedLabelFilter() { function initArchivedLabelFilter() {
const archivedLabelEl = document.querySelector('#archived-filter-checkbox'); const archivedLabelEl = document.querySelector<HTMLInputElement>('#archived-filter-checkbox');
if (!archivedLabelEl) { if (!archivedLabelEl) return;
return;
}
const url = new URL(window.location.href); const url = new URL(window.location.href);
const archivedLabels = document.querySelectorAll('[data-is-archived]'); const archivedLabels = document.querySelectorAll('[data-is-archived]');
@ -219,7 +216,7 @@ function initArchivedLabelFilter() {
} }
const selectedLabels = (url.searchParams.get('labels') || '') const selectedLabels = (url.searchParams.get('labels') || '')
.split(',') .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 = () => { const archivedElToggle = () => {
for (const label of archivedLabels) { for (const label of archivedLabels) {
@ -241,9 +238,9 @@ function initArchivedLabelFilter() {
} }
export function initRepoIssueList() { 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(); initRepoIssueListCheckboxes();
initRepoIssueListAuthorDropdown(); queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
initIssuePinSort(); initIssuePinSort();
initArchivedLabelFilter(); initArchivedLabelFilter();
} }