From 6267713f9f5d831e8a1c7bc07f6ede6f304c6fa7 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Tue, 21 Jan 2025 14:32:49 +0100 Subject: [PATCH 01/10] Allow filtering issues by any assignee This is the opposite of the "No assignee" filter. It will match all issues that have at least one assignee. --- models/db/search.go | 5 +++++ models/issues/issue_search.go | 2 ++ options/locale/locale_en-US.ini | 1 + templates/repo/issue/filter_item_user_assign.tmpl | 4 ++++ templates/repo/issue/filter_list.tmpl | 1 + 5 files changed, 13 insertions(+) diff --git a/models/db/search.go b/models/db/search.go index e0a1b6bde9ffd..fa72959fa5680 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -30,6 +30,11 @@ const ( // eg: "milestone_id=-1" means "find the items without any milestone. const NoConditionID int64 = -1 +// AnyConditionID means a condition to filter the records which match any id. +// The inverse of the above NoConditionID +// eg: "assignee_id=-2" means "find the issues with an assignee" +const AnyConditionID int64 = -2 + // NonExistingID means a condition to match no result (eg: a non-existing user) // It doesn't use -1 or -2 because they are used as builtin users. const NonExistingID int64 = -1000000 diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index f1cd125d495c4..566905b200180 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -359,6 +359,8 @@ func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64 } if assigneeID.Value() == db.NoConditionID { sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") + } else if assigneeID.Value() == db.AnyConditionID { + sess.Where("issue.id IN (SELECT issue_id FROM issue_assignees)") } else { sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). And("issue_assignees.assignee_id = ?", assigneeID.Value()) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 533eb136f94ff..4401660b8acd6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1531,6 +1531,7 @@ issues.filter_project_none = No project issues.filter_assignee = Assignee issues.filter_assginee_no_select = All assignees issues.filter_assginee_no_assignee = No assignee +issues.filter_assignee_any_assignee = Any assignee issues.filter_poster = Author issues.filter_user_placeholder = Search users issues.filter_user_no_select = All users diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl index 4f1db71d57f0a..dd5d96319a403 100644 --- a/templates/repo/issue/filter_item_user_assign.tmpl +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -6,6 +6,7 @@ * TextFilterTitle * TextZeroValue: the text for "all issues" * TextNegativeOne: the text for "issues with no assignee" +* TextNegativeTwo: the text for "issues with any assignee" */}} {{$queryLink := .QueryLink}} diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl index 028a0bfca5a93..c7db7bc9d8fd7 100644 --- a/templates/repo/issue/filter_item_user_assign.tmpl +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -16,19 +16,24 @@ {{svg "octicon-search" 16}} - {{if $.TextZeroValue}} - {{$.TextZeroValue}} - {{end}} - {{if $.TextNegativeOne}} - {{$.TextNegativeOne}} + {{if $.TextFilterMatchNone}} + {{$isSelected := eq .SelectedUserId "(none)"}} + + {{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchNone}} + {{end}} - {{if $.TextAnyCondition}} - {{$.TextAnyCondition}} + {{if $.TextFilterMatchAny}} + {{$isSelected := eq .SelectedUserId "(any)"}} + + {{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchAny}} + {{end}}
- {{range .UserSearchList}} - - {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} + {{range $user := .UserSearchList}} + {{$isSelected := eq $.SelectedUserId (print $user.ID)}} + + {{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} + {{ctx.AvatarUtils.Avatar $user 20}}{{template "repo/search_name" .}} {{end}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index ce45b5d9b6ec4..58ca4a7c00d64 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -94,9 +94,8 @@ "UserSearchList" $.Assignees "SelectedUserId" $.AssigneeID "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") - "TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") - "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") - "TextAnyCondition" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee") + "TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") + "TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee") }} {{if .IsSigned}} diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index d3a1f7dc24977..009b51d8b1fd9 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -1130,7 +1130,11 @@ $.fn.dropdown = function(parameters) { icon: { click: function(event) { iconClicked=true; - if(module.has.search()) { + // GITEA-PATCH: official dropdown doesn't support the search input in menu + // so we need to make the menu could be shown when the search input is in menu and user clicks the icon + const searchInputInMenu = Boolean($menu.find('.search > input').length); + if(module.has.search() && !searchInputInMenu) { + // the search input is in the dropdown element (but not in the popup menu), try to focus it if(!module.is.active()) { if(settings.showOnFocus){ module.focusSearch(); From 9576848b84049c35952366817cdbf233f46b1017 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 20 Mar 2025 16:51:06 +0800 Subject: [PATCH 10/10] fix test and comment --- models/issues/issue_search.go | 2 ++ models/issues/issue_test.go | 3 +-- modules/indexer/issues/indexer_test.go | 10 +++++----- modules/indexer/issues/internal/tests/tests.go | 8 ++++---- routers/api/v1/repo/issue.go | 2 +- routers/web/shared/user/helper.go | 1 + templates/repo/issue/filter_item_user_assign.tmpl | 5 ++--- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 621091d9a10f1..737b69f1547b6 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -369,6 +369,8 @@ func applyAssigneeCondition(sess *xorm.Session, assigneeID string) { } func applyPosterCondition(sess *xorm.Session, posterID string) { + // Actually every issue has a poster. + // The "(none)" is for internal usage only: when doer tries to search non-existing user as poster, use "(none)" to return empty result. if posterID == "(none)" { sess.And("issue.poster_id=0") } else if posterIDInt64, _ := strconv.ParseInt(posterID, 10, 64); posterIDInt64 > 0 { diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 3f76a81bb65d4..c32aa26b2ba66 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -15,7 +15,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -155,7 +154,7 @@ func TestIssues(t *testing.T) { }{ { issues_model.IssuesOptions{ - AssigneeID: optional.Some(int64(1)), + AssigneeID: "1", SortType: "oldest", }, []int64{1, 6}, diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 4bf19e129740f..3e38ac49b719c 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -177,19 +177,19 @@ func searchIssueByID(t *testing.T) { }{ { opts: SearchOptions{ - PosterID: optional.Some(int64(1)), + PosterID: "1", }, expectedIDs: []int64{11, 6, 3, 2, 1}, }, { opts: SearchOptions{ - AssigneeID: optional.Some(int64(1)), + AssigneeID: "1", }, expectedIDs: []int64{6, 1}, }, { - // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1. - opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}), + // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it handles the filter correctly + opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: "(none)"}), expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2}, }, { @@ -472,7 +472,7 @@ func searchIssueWithAnyAssignee(t *testing.T) { }{ { SearchOptions{ - AnyAssigneeOnly: true, + AssigneeID: "(any)", }, []int64{17, 6, 1}, 3, diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index af23ec4e46fef..6e92c7885c886 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -379,7 +379,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - PosterID: optional.Some(int64(1)), + PosterID: "1", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) @@ -397,7 +397,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - AssigneeID: optional.Some(int64(1)), + AssigneeID: "1", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) @@ -415,7 +415,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - AssigneeID: optional.Some(int64(0)), + AssigneeID: "(none)", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) @@ -650,7 +650,7 @@ var cases = []*testIndexerCase{ { Name: "SearchAnyAssignee", SearchOptions: &internal.SearchOptions{ - AnyAssigneeOnly: true, + AssigneeID: "(any)", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 180) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index c0d638b0542a8..e678db526203c 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -541,7 +541,7 @@ func ListIssues(ctx *context.APIContext) { searchOpt.PosterID = strconv.FormatInt(createdByID, 10) } if assignedByID > 0 { - searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 64) + searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10) } if mentionedByID > 0 { searchOpt.MentionID = optional.Some(mentionedByID) diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index dddfc36218258..3fc39fd3ab9b9 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -44,6 +44,7 @@ func GetFilterUserIDByName(ctx context.Context, name string) string { if id, err := strconv.ParseInt(name, 10, 64); err == nil { return strconv.FormatInt(id, 10) } + // The "(none)" is for internal usage only: when doer tries to search non-existing user, use "(none)" to return empty result. return "(none)" } return strconv.FormatInt(u.ID, 10) diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl index c7db7bc9d8fd7..42886edaa06d8 100644 --- a/templates/repo/issue/filter_item_user_assign.tmpl +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -4,9 +4,8 @@ * UserSearchList * SelectedUserId: 0 or empty means default, -1 means "no user is set" * TextFilterTitle -* TextZeroValue: the text for "all issues" -* TextNegativeOne: the text for "issues with no assignee" -* TextAnyCondition: the text for "issues with any assignee" +* TextFilterMatchNone: the text for "issues with no assignee" +* TextFilterMatchAny: the text for "issues with any assignee" */}} {{$queryLink := .QueryLink}}