mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-18 00:47:48 -04: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.
|
// 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.
|
// eg: "milestone_id=-1" means "find the items without any milestone.
|
||||||
const NoConditionID int64 = -1
|
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
|
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||||
AllPublic bool // include also all public repositories
|
AllPublic bool // include also all public repositories
|
||||||
RepoCond builder.Cond
|
RepoCond builder.Cond
|
||||||
AssigneeID optional.Option[int64]
|
AssigneeID string // "(none)" or "(any)" or a user ID
|
||||||
PosterID optional.Option[int64]
|
PosterID string // "(none)" or "(any)" or a user ID
|
||||||
MentionedID int64
|
MentionedID int64
|
||||||
ReviewRequestedID int64
|
ReviewRequestedID int64
|
||||||
ReviewedID int64
|
ReviewedID int64
|
||||||
@ -356,26 +356,25 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod
|
|||||||
return cond
|
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
|
// old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64
|
||||||
if !assigneeID.Has() || assigneeID.Value() == 0 {
|
if assigneeID == "(none)" {
|
||||||
return
|
|
||||||
}
|
|
||||||
if assigneeID.Value() == db.NoConditionID {
|
|
||||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
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").
|
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]) {
|
func applyPosterCondition(sess *xorm.Session, posterID string) {
|
||||||
if !posterID.Has() {
|
// Actually every issue has a poster.
|
||||||
return
|
// 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)" {
|
||||||
// poster doesn't need to support db.NoConditionID(-1), so just use the value as-is
|
sess.And("issue.poster_id=0")
|
||||||
if posterID.Has() {
|
} else if posterIDInt64, _ := strconv.ParseInt(posterID, 10, 64); posterIDInt64 > 0 {
|
||||||
sess.And("issue.poster_id=?", posterID.Value())
|
sess.And("issue.poster_id=?", posterIDInt64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -155,7 +154,7 @@ func TestIssues(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
issues_model.IssuesOptions{
|
issues_model.IssuesOptions{
|
||||||
AssigneeID: optional.Some(int64(1)),
|
AssigneeID: "1",
|
||||||
SortType: "oldest",
|
SortType: "oldest",
|
||||||
},
|
},
|
||||||
[]int64{1, 6},
|
[]int64{1, 6},
|
||||||
|
@ -5,11 +5,13 @@ package bleve
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/indexer"
|
"code.gitea.io/gitea/modules/indexer"
|
||||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||||
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
|
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
|
||||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2"
|
"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"))
|
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PosterID.Has() {
|
if options.PosterID != "" {
|
||||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id"))
|
// "(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() {
|
if options.AssigneeID != "" {
|
||||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id"))
|
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() {
|
if options.MentionID.Has() {
|
||||||
|
@ -54,7 +54,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||||||
RepoIDs: options.RepoIDs,
|
RepoIDs: options.RepoIDs,
|
||||||
AllPublic: options.AllPublic,
|
AllPublic: options.AllPublic,
|
||||||
RepoCond: nil,
|
RepoCond: nil,
|
||||||
AssigneeID: optional.Some(convertID(options.AssigneeID)),
|
AssigneeID: options.AssigneeID,
|
||||||
PosterID: options.PosterID,
|
PosterID: options.PosterID,
|
||||||
MentionedID: convertID(options.MentionID),
|
MentionedID: convertID(options.MentionID),
|
||||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
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)
|
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AssigneeID.Value() == db.NoConditionID {
|
searchOpt.AssigneeID = opts.AssigneeID
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||||
convertID := func(id int64) optional.Option[int64] {
|
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()))
|
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PosterID.Has() {
|
if options.PosterID != "" {
|
||||||
query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value()))
|
// "(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() {
|
if options.AssigneeID != "" {
|
||||||
query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
|
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() {
|
if options.MentionID.Has() {
|
||||||
|
@ -44,6 +44,7 @@ func TestDBSearchIssues(t *testing.T) {
|
|||||||
t.Run("search issues with order", searchIssueWithOrder)
|
t.Run("search issues with order", searchIssueWithOrder)
|
||||||
t.Run("search issues in project", searchIssueInProject)
|
t.Run("search issues in project", searchIssueInProject)
|
||||||
t.Run("search issues with paginator", searchIssueWithPaginator)
|
t.Run("search issues with paginator", searchIssueWithPaginator)
|
||||||
|
t.Run("search issues with any assignee", searchIssueWithAnyAssignee)
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchIssueWithKeyword(t *testing.T) {
|
func searchIssueWithKeyword(t *testing.T) {
|
||||||
@ -176,19 +177,19 @@ func searchIssueByID(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
opts: SearchOptions{
|
opts: SearchOptions{
|
||||||
PosterID: optional.Some(int64(1)),
|
PosterID: "1",
|
||||||
},
|
},
|
||||||
expectedIDs: []int64{11, 6, 3, 2, 1},
|
expectedIDs: []int64{11, 6, 3, 2, 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
opts: SearchOptions{
|
opts: SearchOptions{
|
||||||
AssigneeID: optional.Some(int64(1)),
|
AssigneeID: "1",
|
||||||
},
|
},
|
||||||
expectedIDs: []int64{6, 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.
|
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it handles the filter correctly
|
||||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}),
|
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},
|
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)
|
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
|
ProjectID optional.Option[int64] // project the issues belong to
|
||||||
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
||||||
|
|
||||||
PosterID optional.Option[int64] // poster of the issues
|
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
|
||||||
AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
|
|
||||||
|
|
||||||
MentionID optional.Option[int64] // mentioned user of the issues
|
MentionID optional.Option[int64] // mentioned user of the issues
|
||||||
|
|
||||||
|
@ -379,7 +379,7 @@ var cases = []*testIndexerCase{
|
|||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
PosterID: optional.Some(int64(1)),
|
PosterID: "1",
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Len(t, result.Hits, 5)
|
assert.Len(t, result.Hits, 5)
|
||||||
@ -397,7 +397,7 @@ var cases = []*testIndexerCase{
|
|||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
AssigneeID: optional.Some(int64(1)),
|
AssigneeID: "1",
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Len(t, result.Hits, 5)
|
assert.Len(t, result.Hits, 5)
|
||||||
@ -415,7 +415,7 @@ var cases = []*testIndexerCase{
|
|||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
AssigneeID: optional.Some(int64(0)),
|
AssigneeID: "(none)",
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Len(t, result.Hits, 5)
|
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 {
|
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()))
|
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PosterID.Has() {
|
if options.PosterID != "" {
|
||||||
query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value()))
|
// "(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() {
|
if options.AssigneeID != "" {
|
||||||
query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
|
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() {
|
if options.MentionID.Has() {
|
||||||
|
@ -1547,8 +1547,8 @@ issues.filter_project = Project
|
|||||||
issues.filter_project_all = All projects
|
issues.filter_project_all = All projects
|
||||||
issues.filter_project_none = No project
|
issues.filter_project_none = No project
|
||||||
issues.filter_assignee = Assignee
|
issues.filter_assignee = Assignee
|
||||||
issues.filter_assginee_no_select = All assignees
|
issues.filter_assginee_no_assignee = Assigned to nobody
|
||||||
issues.filter_assginee_no_assignee = No assignee
|
issues.filter_assignee_any_assignee = Assigned to anybody
|
||||||
issues.filter_poster = Author
|
issues.filter_poster = Author
|
||||||
issues.filter_user_placeholder = Search users
|
issues.filter_user_placeholder = Search users
|
||||||
issues.filter_user_no_select = All users
|
issues.filter_user_no_select = All users
|
||||||
|
@ -290,10 +290,10 @@ func SearchIssues(ctx *context.APIContext) {
|
|||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
ctxUserID := ctx.Doer.ID
|
ctxUserID := ctx.Doer.ID
|
||||||
if ctx.FormBool("created") {
|
if ctx.FormBool("created") {
|
||||||
searchOpt.PosterID = optional.Some(ctxUserID)
|
searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
|
||||||
}
|
}
|
||||||
if ctx.FormBool("assigned") {
|
if ctx.FormBool("assigned") {
|
||||||
searchOpt.AssigneeID = optional.Some(ctxUserID)
|
searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
|
||||||
}
|
}
|
||||||
if ctx.FormBool("mentioned") {
|
if ctx.FormBool("mentioned") {
|
||||||
searchOpt.MentionID = optional.Some(ctxUserID)
|
searchOpt.MentionID = optional.Some(ctxUserID)
|
||||||
@ -538,10 +538,10 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if createdByID > 0 {
|
if createdByID > 0 {
|
||||||
searchOpt.PosterID = optional.Some(createdByID)
|
searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
|
||||||
}
|
}
|
||||||
if assignedByID > 0 {
|
if assignedByID > 0 {
|
||||||
searchOpt.AssigneeID = optional.Some(assignedByID)
|
searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
|
||||||
}
|
}
|
||||||
if mentionedByID > 0 {
|
if mentionedByID > 0 {
|
||||||
searchOpt.MentionID = optional.Some(mentionedByID)
|
searchOpt.MentionID = optional.Some(mentionedByID)
|
||||||
|
@ -347,11 +347,11 @@ func ViewProject(ctx *context.Context) {
|
|||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
assigneeID := ctx.FormString("assignee")
|
||||||
|
|
||||||
opts := issues_model.IssuesOptions{
|
opts := issues_model.IssuesOptions{
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
AssigneeID: optional.Some(assigneeID),
|
AssigneeID: assigneeID,
|
||||||
Owner: project.Owner,
|
Owner: project.Owner,
|
||||||
Doer: ctx.Doer,
|
Doer: ctx.Doer,
|
||||||
}
|
}
|
||||||
|
@ -208,10 +208,10 @@ func SearchIssues(ctx *context.Context) {
|
|||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
ctxUserID := ctx.Doer.ID
|
ctxUserID := ctx.Doer.ID
|
||||||
if ctx.FormBool("created") {
|
if ctx.FormBool("created") {
|
||||||
searchOpt.PosterID = optional.Some(ctxUserID)
|
searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
|
||||||
}
|
}
|
||||||
if ctx.FormBool("assigned") {
|
if ctx.FormBool("assigned") {
|
||||||
searchOpt.AssigneeID = optional.Some(ctxUserID)
|
searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
|
||||||
}
|
}
|
||||||
if ctx.FormBool("mentioned") {
|
if ctx.FormBool("mentioned") {
|
||||||
searchOpt.MentionID = optional.Some(ctxUserID)
|
searchOpt.MentionID = optional.Some(ctxUserID)
|
||||||
@ -373,10 +373,10 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if createdByID > 0 {
|
if createdByID > 0 {
|
||||||
searchOpt.PosterID = optional.Some(createdByID)
|
searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
|
||||||
}
|
}
|
||||||
if assignedByID > 0 {
|
if assignedByID > 0 {
|
||||||
searchOpt.AssigneeID = optional.Some(assignedByID)
|
searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
|
||||||
}
|
}
|
||||||
if mentionedByID > 0 {
|
if mentionedByID > 0 {
|
||||||
searchOpt.MentionID = optional.Some(mentionedByID)
|
searchOpt.MentionID = optional.Some(mentionedByID)
|
||||||
@ -490,7 +490,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
viewType = "all"
|
viewType = "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
assigneeID := ctx.FormString("assignee")
|
||||||
posterUsername := ctx.FormString("poster")
|
posterUsername := ctx.FormString("poster")
|
||||||
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
|
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
|
||||||
var mentionedID, reviewRequestedID, reviewedID int64
|
var mentionedID, reviewRequestedID, reviewedID int64
|
||||||
@ -498,11 +498,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
switch viewType {
|
switch viewType {
|
||||||
case "created_by":
|
case "created_by":
|
||||||
posterUserID = optional.Some(ctx.Doer.ID)
|
posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
|
||||||
case "mentioned":
|
case "mentioned":
|
||||||
mentionedID = ctx.Doer.ID
|
mentionedID = ctx.Doer.ID
|
||||||
case "assigned":
|
case "assigned":
|
||||||
assigneeID = ctx.Doer.ID
|
assigneeID = fmt.Sprint(ctx.Doer.ID)
|
||||||
case "review_requested":
|
case "review_requested":
|
||||||
reviewRequestedID = ctx.Doer.ID
|
reviewRequestedID = ctx.Doer.ID
|
||||||
case "reviewed_by":
|
case "reviewed_by":
|
||||||
@ -532,7 +532,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
MilestoneIDs: mileIDs,
|
MilestoneIDs: mileIDs,
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
AssigneeID: optional.Some(assigneeID),
|
AssigneeID: assigneeID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
PosterID: posterUserID,
|
PosterID: posterUserID,
|
||||||
ReviewRequestedID: reviewRequestedID,
|
ReviewRequestedID: reviewRequestedID,
|
||||||
@ -613,7 +613,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
},
|
},
|
||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
AssigneeID: optional.Some(assigneeID),
|
AssigneeID: assigneeID,
|
||||||
PosterID: posterUserID,
|
PosterID: posterUserID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
ReviewRequestedID: reviewRequestedID,
|
ReviewRequestedID: reviewRequestedID,
|
||||||
|
@ -315,12 +315,12 @@ func ViewProject(ctx *context.Context) {
|
|||||||
|
|
||||||
labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
|
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{
|
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
|
||||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
AssigneeID: optional.Some(assigneeID),
|
AssigneeID: assigneeID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
|
@ -8,9 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
"code.gitea.io/gitea/models/user"
|
"code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
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.
|
// 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.
|
// 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:
|
// Return values:
|
||||||
// * nil: no filter
|
// * "": no filter
|
||||||
// * some(id): match the id, the id could be -1 to match the issues without assignee
|
// * "{the-id}": match the id
|
||||||
// * some(NonExistingID): match no issue (due to the user doesn't exist)
|
// * "(none)": match no issue (due to the user doesn't exist)
|
||||||
func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] {
|
func GetFilterUserIDByName(ctx context.Context, name string) string {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return optional.None[int64]()
|
return ""
|
||||||
}
|
}
|
||||||
u, err := user.GetUserByName(ctx, name)
|
u, err := user.GetUserByName(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if id, err := strconv.ParseInt(name, 10, 64); 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.FilterModeAll:
|
||||||
case issues_model.FilterModeYourRepositories:
|
case issues_model.FilterModeYourRepositories:
|
||||||
case issues_model.FilterModeAssign:
|
case issues_model.FilterModeAssign:
|
||||||
opts.AssigneeID = optional.Some(ctx.Doer.ID)
|
opts.AssigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
|
||||||
case issues_model.FilterModeCreate:
|
case issues_model.FilterModeCreate:
|
||||||
opts.PosterID = optional.Some(ctx.Doer.ID)
|
opts.PosterID = strconv.FormatInt(ctx.Doer.ID, 10)
|
||||||
case issues_model.FilterModeMention:
|
case issues_model.FilterModeMention:
|
||||||
opts.MentionedID = ctx.Doer.ID
|
opts.MentionedID = ctx.Doer.ID
|
||||||
case issues_model.FilterModeReviewRequested:
|
case issues_model.FilterModeReviewRequested:
|
||||||
@ -792,9 +792,9 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
|
|||||||
case issues_model.FilterModeYourRepositories:
|
case issues_model.FilterModeYourRepositories:
|
||||||
openClosedOpts.AllPublic = false
|
openClosedOpts.AllPublic = false
|
||||||
case issues_model.FilterModeAssign:
|
case issues_model.FilterModeAssign:
|
||||||
openClosedOpts.AssigneeID = optional.Some(doerID)
|
openClosedOpts.AssigneeID = strconv.FormatInt(doerID, 10)
|
||||||
case issues_model.FilterModeCreate:
|
case issues_model.FilterModeCreate:
|
||||||
openClosedOpts.PosterID = optional.Some(doerID)
|
openClosedOpts.PosterID = strconv.FormatInt(doerID, 10)
|
||||||
case issues_model.FilterModeMention:
|
case issues_model.FilterModeMention:
|
||||||
openClosedOpts.MentionID = optional.Some(doerID)
|
openClosedOpts.MentionID = optional.Some(doerID)
|
||||||
case issues_model.FilterModeReviewRequested:
|
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
|
// Below stats are for the left sidebar
|
||||||
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
|
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
|
||||||
o.AssigneeID = nil
|
o.AssigneeID = ""
|
||||||
o.PosterID = nil
|
o.PosterID = ""
|
||||||
o.MentionID = nil
|
o.MentionID = nil
|
||||||
o.ReviewRequestedID = nil
|
o.ReviewRequestedID = nil
|
||||||
o.ReviewedID = nil
|
o.ReviewedID = nil
|
||||||
@ -827,11 +827,11 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
"UserSearchList" $.Assignees
|
"UserSearchList" $.Assignees
|
||||||
"SelectedUserId" $.AssigneeID
|
"SelectedUserId" $.AssigneeID
|
||||||
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
|
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
|
||||||
"TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select")
|
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
||||||
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
* UserSearchList
|
* UserSearchList
|
||||||
* SelectedUserId: 0 or empty means default, -1 means "no user is set"
|
* SelectedUserId: 0 or empty means default, -1 means "no user is set"
|
||||||
* TextFilterTitle
|
* TextFilterTitle
|
||||||
* TextZeroValue: the text for "all issues"
|
* TextFilterMatchNone: the text for "issues with no assignee"
|
||||||
* TextNegativeOne: the text for "issues with no assignee"
|
* TextFilterMatchAny: the text for "issues with any assignee"
|
||||||
*/}}
|
*/}}
|
||||||
{{$queryLink := .QueryLink}}
|
{{$queryLink := .QueryLink}}
|
||||||
<div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}">
|
<div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}">
|
||||||
@ -15,16 +15,24 @@
|
|||||||
<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_user_placeholder"}}">
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}">
|
||||||
</div>
|
</div>
|
||||||
{{if $.TextZeroValue}}
|
{{if $.TextFilterMatchNone}}
|
||||||
<a class="item {{if not .SelectedUserId}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey NIL}}">{{$.TextZeroValue}}</a>
|
{{$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}}
|
{{end}}
|
||||||
{{if $.TextNegativeOne}}
|
{{if $.TextFilterMatchAny}}
|
||||||
<a class="item {{if eq .SelectedUserId -1}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey -1}}">{{$.TextNegativeOne}}</a>
|
{{$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}}
|
{{end}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{range .UserSearchList}}
|
{{range $user := .UserSearchList}}
|
||||||
<a class="item {{if eq $.SelectedUserId .ID}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey .ID}}">
|
{{$isSelected := eq $.SelectedUserId (print $user.ID)}}
|
||||||
{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
|
<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>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,8 +94,8 @@
|
|||||||
"UserSearchList" $.Assignees
|
"UserSearchList" $.Assignees
|
||||||
"SelectedUserId" $.AssigneeID
|
"SelectedUserId" $.AssigneeID
|
||||||
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
|
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
|
||||||
"TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select")
|
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
||||||
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{{if .IsSigned}}
|
{{if .IsSigned}}
|
||||||
|
@ -1130,7 +1130,11 @@ $.fn.dropdown = function(parameters) {
|
|||||||
icon: {
|
icon: {
|
||||||
click: function(event) {
|
click: function(event) {
|
||||||
iconClicked=true;
|
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(!module.is.active()) {
|
||||||
if(settings.showOnFocus){
|
if(settings.showOnFocus){
|
||||||
module.focusSearch();
|
module.focusSearch();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user