Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add label/author/assignee filters to the user/org home issue list #32779

Merged
merged 4 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions models/db/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ const (
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
)

const (
// Which means a condition to filter the records which don't match any id.
// It's different from zero which means the condition could be ignored.
NoConditionID = -1
)
// 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
39 changes: 23 additions & 16 deletions models/issues/issue_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 int64
PosterID int64
AssigneeID optional.Option[int64]
PosterID optional.Option[int64]
MentionedID int64
ReviewRequestedID int64
ReviewedID int64
Expand Down Expand Up @@ -231,15 +231,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
sess.And("issue.is_closed=?", opts.IsClosed.Value())
}

if opts.AssigneeID > 0 {
applyAssigneeCondition(sess, opts.AssigneeID)
} else if opts.AssigneeID == db.NoConditionID {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
}

if opts.PosterID > 0 {
applyPosterCondition(sess, opts.PosterID)
}
applyAssigneeCondition(sess, opts.AssigneeID)
applyPosterCondition(sess, opts.PosterID)

if opts.MentionedID > 0 {
applyMentionedCondition(sess, opts.MentionedID)
Expand Down Expand Up @@ -359,13 +352,27 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati
return cond
}

func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", assigneeID)
func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) {
// 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 {
sess.Where("issue.id NOT 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())
}
}

func applyPosterCondition(sess *xorm.Session, posterID int64) {
sess.And("issue.poster_id=?", posterID)
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 applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
Expand Down
10 changes: 2 additions & 8 deletions models/issues/issue_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,9 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6

applyProjectCondition(sess, opts)

if opts.AssigneeID > 0 {
applyAssigneeCondition(sess, opts.AssigneeID)
} else if opts.AssigneeID == db.NoConditionID {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
}
applyAssigneeCondition(sess, opts.AssigneeID)

if opts.PosterID > 0 {
applyPosterCondition(sess, opts.PosterID)
}
applyPosterCondition(sess, opts.PosterID)

if opts.MentionedID > 0 {
applyMentionedCondition(sess, opts.MentionedID)
Expand Down
3 changes: 2 additions & 1 deletion models/issues/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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"
Expand Down Expand Up @@ -155,7 +156,7 @@ func TestIssues(t *testing.T) {
}{
{
issues_model.IssuesOptions{
AssigneeID: 1,
AssigneeID: optional.Some(int64(1)),
SortType: "oldest",
},
[]int64{1, 6},
Expand Down
4 changes: 2 additions & 2 deletions modules/indexer/issues/db/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
RepoIDs: options.RepoIDs,
AllPublic: options.AllPublic,
RepoCond: nil,
AssigneeID: convertID(options.AssigneeID),
PosterID: convertID(options.PosterID),
AssigneeID: optional.Some(convertID(options.AssigneeID)),
PosterID: options.PosterID,
MentionedID: convertID(options.MentionID),
ReviewRequestedID: convertID(options.ReviewRequestedID),
ReviewedID: convertID(options.ReviewedID),
Expand Down
12 changes: 6 additions & 6 deletions modules/indexer/issues/dboptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp

if opts.ProjectID > 0 {
searchOpt.ProjectID = optional.Some(opts.ProjectID)
} else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places
} else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
}

if opts.AssigneeID > 0 {
searchOpt.AssigneeID = optional.Some(opts.AssigneeID)
} else if opts.AssigneeID == -1 { // FIXME: this is inconsistent from other places
searchOpt.AssigneeID = optional.Some[int64](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
}

// See the comment of issues_model.SearchOptions for the reason why we need to convert
Expand All @@ -62,7 +62,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
}

searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
searchOpt.PosterID = convertID(opts.PosterID)
searchOpt.PosterID = opts.PosterID
searchOpt.MentionID = convertID(opts.MentionedID)
searchOpt.ReviewedID = convertID(opts.ReviewedID)
searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID)
Expand Down
2 changes: 1 addition & 1 deletion modules/indexer/issues/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func searchIssueByID(t *testing.T) {
},
{
// 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: -1}),
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}),
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
},
{
Expand Down
21 changes: 6 additions & 15 deletions routers/web/org/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/shared/issue"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
Expand Down Expand Up @@ -334,23 +335,15 @@ func ViewProject(ctx *context.Context) {
return
}

var labelIDs []int64
// 1,-2 means including label 1 and excluding label 2
// 0 means issues with no label
// blank means labels will not be filtered for issues
selectLabels := ctx.FormString("labels")
if selectLabels != "" {
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
if err != nil {
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
}
labelIDs := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner)
if ctx.Written() {
return
}

assigneeID := ctx.FormInt64("assignee")
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future

issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
LabelIDs: labelIDs,
AssigneeID: assigneeID,
AssigneeID: optional.Some(assigneeID),
})
if err != nil {
ctx.ServerError("LoadIssuesOfColumns", err)
Expand Down Expand Up @@ -426,8 +419,6 @@ func ViewProject(ctx *context.Context) {
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)

ctx.Data["SelectLabels"] = selectLabels
ctx.Data["AssigneeID"] = assigneeID

project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description)
Expand Down
Loading
Loading