mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-13 08:58:12 +00:00
Allow filtering issues by any assignee (#33343)
This is the opposite of the "No assignee" filter, it will match all issues that have at least one assignee. Before  After  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
a4df01b580
commit
0da7318cf3
@ -29,7 +29,3 @@ const (
|
||||
// NoConditionID means a condition to filter the records which don't match any id.
|
||||
// eg: "milestone_id=-1" means "find the items without any milestone.
|
||||
const NoConditionID int64 = -1
|
||||
|
||||
// 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
|
||||
|
@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint
|
||||
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||
AllPublic bool // include also all public repositories
|
||||
RepoCond builder.Cond
|
||||
AssigneeID optional.Option[int64]
|
||||
PosterID optional.Option[int64]
|
||||
AssigneeID string // "(none)" or "(any)" or a user ID
|
||||
PosterID string // "(none)" or "(any)" or a user ID
|
||||
MentionedID int64
|
||||
ReviewRequestedID int64
|
||||
ReviewedID int64
|
||||
@ -356,26 +356,25 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod
|
||||
return cond
|
||||
}
|
||||
|
||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) {
|
||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID string) {
|
||||
// old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64
|
||||
if !assigneeID.Has() || assigneeID.Value() == 0 {
|
||||
return
|
||||
}
|
||||
if assigneeID.Value() == db.NoConditionID {
|
||||
if assigneeID == "(none)" {
|
||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
||||
} else {
|
||||
} else if assigneeID == "(any)" {
|
||||
sess.Where("issue.id IN (SELECT issue_id FROM issue_assignees)")
|
||||
} else if assigneeIDInt64, _ := strconv.ParseInt(assigneeID, 10, 64); assigneeIDInt64 > 0 {
|
||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", assigneeID.Value())
|
||||
And("issue_assignees.assignee_id = ?", assigneeIDInt64)
|
||||
}
|
||||
}
|
||||
|
||||
func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) {
|
||||
if !posterID.Has() {
|
||||
return
|
||||
}
|
||||
// poster doesn't need to support db.NoConditionID(-1), so just use the value as-is
|
||||
if posterID.Has() {
|
||||
sess.And("issue.poster_id=?", posterID.Value())
|
||||
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 {
|
||||
sess.And("issue.poster_id=?", posterIDInt64)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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},
|
||||
|
@ -5,11 +5,13 @@ package bleve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/indexer"
|
||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
@ -246,12 +248,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id"))
|
||||
if options.PosterID != "" {
|
||||
// "(none)" becomes 0, it means no poster
|
||||
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(posterIDInt64, "poster_id"))
|
||||
}
|
||||
|
||||
if options.AssigneeID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id"))
|
||||
if options.AssigneeID != "" {
|
||||
if options.AssigneeID == "(any)" {
|
||||
queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(optional.Some[int64](1), optional.None[int64](), "assignee_id"))
|
||||
} else {
|
||||
// "(none)" becomes 0, it means no assignee
|
||||
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(assigneeIDInt64, "assignee_id"))
|
||||
}
|
||||
}
|
||||
|
||||
if options.MentionID.Has() {
|
||||
|
@ -54,7 +54,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||
RepoIDs: options.RepoIDs,
|
||||
AllPublic: options.AllPublic,
|
||||
RepoCond: nil,
|
||||
AssigneeID: optional.Some(convertID(options.AssigneeID)),
|
||||
AssigneeID: options.AssigneeID,
|
||||
PosterID: options.PosterID,
|
||||
MentionedID: convertID(options.MentionID),
|
||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
||||
|
@ -45,11 +45,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||
}
|
||||
|
||||
if opts.AssigneeID.Value() == db.NoConditionID {
|
||||
searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee"
|
||||
} else if opts.AssigneeID.Value() != 0 {
|
||||
searchOpt.AssigneeID = opts.AssigneeID
|
||||
}
|
||||
searchOpt.AssigneeID = opts.AssigneeID
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
convertID := func(id int64) optional.Option[int64] {
|
||||
|
@ -212,12 +212,22 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value()))
|
||||
if options.PosterID != "" {
|
||||
// "(none)" becomes 0, it means no poster
|
||||
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
|
||||
query.Must(elastic.NewTermQuery("poster_id", posterIDInt64))
|
||||
}
|
||||
|
||||
if options.AssigneeID.Has() {
|
||||
query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
|
||||
if options.AssigneeID != "" {
|
||||
if options.AssigneeID == "(any)" {
|
||||
q := elastic.NewRangeQuery("assignee_id")
|
||||
q.Gte(1)
|
||||
query.Must(q)
|
||||
} else {
|
||||
// "(none)" becomes 0, it means no assignee
|
||||
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
|
||||
query.Must(elastic.NewTermQuery("assignee_id", assigneeIDInt64))
|
||||
}
|
||||
}
|
||||
|
||||
if options.MentionID.Has() {
|
||||
|
@ -44,6 +44,7 @@ func TestDBSearchIssues(t *testing.T) {
|
||||
t.Run("search issues with order", searchIssueWithOrder)
|
||||
t.Run("search issues in project", searchIssueInProject)
|
||||
t.Run("search issues with paginator", searchIssueWithPaginator)
|
||||
t.Run("search issues with any assignee", searchIssueWithAnyAssignee)
|
||||
}
|
||||
|
||||
func searchIssueWithKeyword(t *testing.T) {
|
||||
@ -176,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},
|
||||
},
|
||||
{
|
||||
@ -462,3 +463,25 @@ func searchIssueWithPaginator(t *testing.T) {
|
||||
assert.Equal(t, test.expectedTotal, total)
|
||||
}
|
||||
}
|
||||
|
||||
func searchIssueWithAnyAssignee(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts SearchOptions
|
||||
expectedIDs []int64
|
||||
expectedTotal int64
|
||||
}{
|
||||
{
|
||||
SearchOptions{
|
||||
AssigneeID: "(any)",
|
||||
},
|
||||
[]int64{17, 6, 1},
|
||||
3,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
issueIDs, total, err := SearchIssues(t.Context(), &test.opts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedIDs, issueIDs)
|
||||
assert.Equal(t, test.expectedTotal, total)
|
||||
}
|
||||
}
|
||||
|
@ -97,9 +97,8 @@ type SearchOptions struct {
|
||||
ProjectID optional.Option[int64] // project the issues belong to
|
||||
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
||||
|
||||
PosterID optional.Option[int64] // poster of the issues
|
||||
|
||||
AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
|
||||
PosterID string // poster of the issues, "(none)" or "(any)" or a user ID
|
||||
AssigneeID string // assignee of the issues, "(none)" or "(any)" or a user ID
|
||||
|
||||
MentionID optional.Option[int64] // mentioned user of the issues
|
||||
|
||||
|
@ -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)
|
||||
@ -647,6 +647,21 @@ var cases = []*testIndexerCase{
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SearchAnyAssignee",
|
||||
SearchOptions: &internal.SearchOptions{
|
||||
AssigneeID: "(any)",
|
||||
},
|
||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||
assert.Len(t, result.Hits, 180)
|
||||
for _, v := range result.Hits {
|
||||
assert.GreaterOrEqual(t, data[v.ID].AssigneeID, int64(1))
|
||||
}
|
||||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
||||
return v.AssigneeID >= 1
|
||||
}), result.Total)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type testIndexerCase struct {
|
||||
|
@ -187,12 +187,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value()))
|
||||
if options.PosterID != "" {
|
||||
// "(none)" becomes 0, it means no poster
|
||||
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
|
||||
query.And(inner_meilisearch.NewFilterEq("poster_id", posterIDInt64))
|
||||
}
|
||||
|
||||
if options.AssigneeID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
|
||||
if options.AssigneeID != "" {
|
||||
if options.AssigneeID == "(any)" {
|
||||
query.And(inner_meilisearch.NewFilterGte("assignee_id", 1))
|
||||
} else {
|
||||
// "(none)" becomes 0, it means no assignee
|
||||
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
|
||||
query.And(inner_meilisearch.NewFilterEq("assignee_id", assigneeIDInt64))
|
||||
}
|
||||
}
|
||||
|
||||
if options.MentionID.Has() {
|
||||
|
@ -1547,8 +1547,8 @@ issues.filter_project = Project
|
||||
issues.filter_project_all = All projects
|
||||
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_assginee_no_assignee = Assigned to nobody
|
||||
issues.filter_assignee_any_assignee = Assigned to anybody
|
||||
issues.filter_poster = Author
|
||||
issues.filter_user_placeholder = Search users
|
||||
issues.filter_user_no_select = All users
|
||||
|
@ -290,10 +290,10 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
if ctx.IsSigned {
|
||||
ctxUserID := ctx.Doer.ID
|
||||
if ctx.FormBool("created") {
|
||||
searchOpt.PosterID = optional.Some(ctxUserID)
|
||||
searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
|
||||
}
|
||||
if ctx.FormBool("assigned") {
|
||||
searchOpt.AssigneeID = optional.Some(ctxUserID)
|
||||
searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
|
||||
}
|
||||
if ctx.FormBool("mentioned") {
|
||||
searchOpt.MentionID = optional.Some(ctxUserID)
|
||||
@ -538,10 +538,10 @@ func ListIssues(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if createdByID > 0 {
|
||||
searchOpt.PosterID = optional.Some(createdByID)
|
||||
searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
|
||||
}
|
||||
if assignedByID > 0 {
|
||||
searchOpt.AssigneeID = optional.Some(assignedByID)
|
||||
searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
|
||||
}
|
||||
if mentionedByID > 0 {
|
||||
searchOpt.MentionID = optional.Some(mentionedByID)
|
||||
|
@ -347,11 +347,11 @@ func ViewProject(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
|
||||
opts := issues_model.IssuesOptions{
|
||||
LabelIDs: labelIDs,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
AssigneeID: assigneeID,
|
||||
Owner: project.Owner,
|
||||
Doer: ctx.Doer,
|
||||
}
|
||||
|
@ -208,10 +208,10 @@ func SearchIssues(ctx *context.Context) {
|
||||
if ctx.IsSigned {
|
||||
ctxUserID := ctx.Doer.ID
|
||||
if ctx.FormBool("created") {
|
||||
searchOpt.PosterID = optional.Some(ctxUserID)
|
||||
searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
|
||||
}
|
||||
if ctx.FormBool("assigned") {
|
||||
searchOpt.AssigneeID = optional.Some(ctxUserID)
|
||||
searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
|
||||
}
|
||||
if ctx.FormBool("mentioned") {
|
||||
searchOpt.MentionID = optional.Some(ctxUserID)
|
||||
@ -373,10 +373,10 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if createdByID > 0 {
|
||||
searchOpt.PosterID = optional.Some(createdByID)
|
||||
searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
|
||||
}
|
||||
if assignedByID > 0 {
|
||||
searchOpt.AssigneeID = optional.Some(assignedByID)
|
||||
searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
|
||||
}
|
||||
if mentionedByID > 0 {
|
||||
searchOpt.MentionID = optional.Some(mentionedByID)
|
||||
@ -490,7 +490,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
viewType = "all"
|
||||
}
|
||||
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
posterUsername := ctx.FormString("poster")
|
||||
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
|
||||
var mentionedID, reviewRequestedID, reviewedID int64
|
||||
@ -498,11 +498,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
if ctx.IsSigned {
|
||||
switch viewType {
|
||||
case "created_by":
|
||||
posterUserID = optional.Some(ctx.Doer.ID)
|
||||
posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
|
||||
case "mentioned":
|
||||
mentionedID = ctx.Doer.ID
|
||||
case "assigned":
|
||||
assigneeID = ctx.Doer.ID
|
||||
assigneeID = fmt.Sprint(ctx.Doer.ID)
|
||||
case "review_requested":
|
||||
reviewRequestedID = ctx.Doer.ID
|
||||
case "reviewed_by":
|
||||
@ -532,7 +532,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
LabelIDs: labelIDs,
|
||||
MilestoneIDs: mileIDs,
|
||||
ProjectID: projectID,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
AssigneeID: assigneeID,
|
||||
MentionedID: mentionedID,
|
||||
PosterID: posterUserID,
|
||||
ReviewRequestedID: reviewRequestedID,
|
||||
@ -613,7 +613,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
RepoIDs: []int64{repo.ID},
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
AssigneeID: assigneeID,
|
||||
PosterID: posterUserID,
|
||||
MentionedID: mentionedID,
|
||||
ReviewRequestedID: reviewRequestedID,
|
||||
|
@ -315,12 +315,12 @@ func ViewProject(ctx *context.Context) {
|
||||
|
||||
labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
|
||||
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
LabelIDs: labelIDs,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
AssigneeID: assigneeID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
|
@ -8,9 +8,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||
@ -34,19 +32,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||
// 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.
|
||||
// Return values:
|
||||
// * nil: no filter
|
||||
// * some(id): match the id, the id could be -1 to match the issues without assignee
|
||||
// * some(NonExistingID): match no issue (due to the user doesn't exist)
|
||||
func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] {
|
||||
// * "": no filter
|
||||
// * "{the-id}": match the id
|
||||
// * "(none)": match no issue (due to the user doesn't exist)
|
||||
func GetFilterUserIDByName(ctx context.Context, name string) string {
|
||||
if name == "" {
|
||||
return optional.None[int64]()
|
||||
return ""
|
||||
}
|
||||
u, err := user.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
|
||||
return optional.Some(id)
|
||||
return strconv.FormatInt(id, 10)
|
||||
}
|
||||
return optional.Some(db.NonExistingID)
|
||||
// The "(none)" is for internal usage only: when doer tries to search non-existing user, use "(none)" to return empty result.
|
||||
return "(none)"
|
||||
}
|
||||
return optional.Some(u.ID)
|
||||
return strconv.FormatInt(u.ID, 10)
|
||||
}
|
||||
|
@ -501,9 +501,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
case issues_model.FilterModeAll:
|
||||
case issues_model.FilterModeYourRepositories:
|
||||
case issues_model.FilterModeAssign:
|
||||
opts.AssigneeID = optional.Some(ctx.Doer.ID)
|
||||
opts.AssigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
|
||||
case issues_model.FilterModeCreate:
|
||||
opts.PosterID = optional.Some(ctx.Doer.ID)
|
||||
opts.PosterID = strconv.FormatInt(ctx.Doer.ID, 10)
|
||||
case issues_model.FilterModeMention:
|
||||
opts.MentionedID = ctx.Doer.ID
|
||||
case issues_model.FilterModeReviewRequested:
|
||||
@ -792,9 +792,9 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
|
||||
case issues_model.FilterModeYourRepositories:
|
||||
openClosedOpts.AllPublic = false
|
||||
case issues_model.FilterModeAssign:
|
||||
openClosedOpts.AssigneeID = optional.Some(doerID)
|
||||
openClosedOpts.AssigneeID = strconv.FormatInt(doerID, 10)
|
||||
case issues_model.FilterModeCreate:
|
||||
openClosedOpts.PosterID = optional.Some(doerID)
|
||||
openClosedOpts.PosterID = strconv.FormatInt(doerID, 10)
|
||||
case issues_model.FilterModeMention:
|
||||
openClosedOpts.MentionID = optional.Some(doerID)
|
||||
case issues_model.FilterModeReviewRequested:
|
||||
@ -816,8 +816,8 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
|
||||
|
||||
// Below stats are for the left sidebar
|
||||
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
|
||||
o.AssigneeID = nil
|
||||
o.PosterID = nil
|
||||
o.AssigneeID = ""
|
||||
o.PosterID = ""
|
||||
o.MentionID = nil
|
||||
o.ReviewRequestedID = nil
|
||||
o.ReviewedID = nil
|
||||
@ -827,11 +827,11 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
|
||||
ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = strconv.FormatInt(doerID, 10) }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
|
||||
ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = strconv.FormatInt(doerID, 10) }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -15,8 +15,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")
|
||||
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
||||
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,8 +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"
|
||||
* TextFilterMatchNone: the text for "issues with no assignee"
|
||||
* TextFilterMatchAny: the text for "issues with any assignee"
|
||||
*/}}
|
||||
{{$queryLink := .QueryLink}}
|
||||
<div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}">
|
||||
@ -15,16 +15,24 @@
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}">
|
||||
</div>
|
||||
{{if $.TextZeroValue}}
|
||||
<a class="item {{if not .SelectedUserId}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey NIL}}">{{$.TextZeroValue}}</a>
|
||||
{{if $.TextFilterMatchNone}}
|
||||
{{$isSelected := eq .SelectedUserId "(none)"}}
|
||||
<a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL "(none)")}}">
|
||||
{{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchNone}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if $.TextNegativeOne}}
|
||||
<a class="item {{if eq .SelectedUserId -1}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey -1}}">{{$.TextNegativeOne}}</a>
|
||||
{{if $.TextFilterMatchAny}}
|
||||
{{$isSelected := eq .SelectedUserId "(any)"}}
|
||||
<a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL "(any)")}}">
|
||||
{{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchAny}}
|
||||
</a>
|
||||
{{end}}
|
||||
<div class="divider"></div>
|
||||
{{range .UserSearchList}}
|
||||
<a class="item {{if eq $.SelectedUserId .ID}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey .ID}}">
|
||||
{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
|
||||
{{range $user := .UserSearchList}}
|
||||
{{$isSelected := eq $.SelectedUserId (print $user.ID)}}
|
||||
<a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL $user.ID)}}">
|
||||
{{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}}
|
||||
{{ctx.AvatarUtils.Avatar $user 20}}{{template "repo/search_name" .}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -94,8 +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")
|
||||
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
||||
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
|
||||
}}
|
||||
|
||||
{{if .IsSigned}}
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user