From 23471e1333b8289063e97cf27b6ad7796f593b47 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 8 Dec 2024 20:44:17 +0800 Subject: [PATCH] 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: --- modules/templates/helper.go | 66 ++++++++++++++ routers/web/repo/issue_list.go | 31 +++---- routers/web/shared/user/helper.go | 21 +++++ routers/web/user/home.go | 70 ++++++++------- templates/repo/issue/filter_list.tmpl | 59 +++++++------ templates/repo/issue/search.tmpl | 2 +- templates/user/dashboard/issues.tmpl | 41 +++++---- web_src/js/features/repo-issue-list.ts | 117 ++++++++++++------------- 8 files changed, 249 insertions(+), 158 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)}} {{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}
- {{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} - {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} + {{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} + {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} @@ -32,7 +33,7 @@
{{end}} {{$previousExclusiveScope = $exclusiveScope}} - + {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} @@ -62,13 +63,13 @@
-
{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}} - {{ctx.Locale.Tr "repo.issues.filter_milestone_none"}} + {{ctx.Locale.Tr "repo.issues.filter_milestone_all"}} + {{ctx.Locale.Tr "repo.issues.filter_milestone_none"}} {{if .OpenMilestones}}
{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}
{{range .OpenMilestones}} - + {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} @@ -78,7 +79,7 @@
{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}
{{range .ClosedMilestones}} - + {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} @@ -99,15 +100,15 @@ {{svg "octicon-search" 16}} - {{ctx.Locale.Tr "repo.issues.filter_project_all"}} - {{ctx.Locale.Tr "repo.issues.filter_project_none"}} + {{ctx.Locale.Tr "repo.issues.filter_project_all"}} + {{ctx.Locale.Tr "repo.issues.filter_project_none"}} {{if .OpenProjects}}
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
{{range .OpenProjects}} - + {{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}{{.Title}} {{end}} @@ -118,7 +119,7 @@ {{ctx.Locale.Tr "repo.issues.new.closed_projects"}} {{range .ClosedProjects}} - + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} {{end}} @@ -130,7 +131,7 @@ - {{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}} - {{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}} + {{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}} + {{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}
{{range .Assignees}} - + {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} {{end}} @@ -175,14 +176,14 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}} {{end}} @@ -194,13 +195,13 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}} 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 @@ - + {{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 @@
{{template "base/alert" .}}
+ {{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "fuzzy" $.IsFuzzy}} + + {{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.SearchFilterPosterUsername "assignee" $.SearchFilterAssigneeID}}
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('.issue-checkbox-all'); if (!issueSelectAll) return; // logged out state - const issueCheckboxes = document.querySelectorAll('.issue-checkbox'); + const issueCheckboxes = document.querySelectorAll('.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; - - // 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 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; - try { - await updateIssuesMeta(url, action, issueIDs, elementId); - window.location.reload(); - } catch (err) { - showErrorToast(err.responseJSON?.error ?? err.message); - } - }); + // 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 = 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() { - 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 = `${htmlEscape(item.username)}`; if (item.full_name) html += `${htmlEscape(item.full_name)}`; - 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('#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(); }