From b807d2f6205bf1ba60d3a543e8e1a16f7be956df Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 17 May 2023 17:21:35 +0800 Subject: [PATCH] Support no label/assignee filter and batch clearing labels/assignees (#24707) Since milestones has been implemented, this PR will fix #3407 --------- Co-authored-by: Jason Song --- models/issues/issue.go | 40 +++++++++++++++++++++------------ options/locale/locale_en-US.ini | 2 ++ routers/web/repo/issue.go | 5 ++++- templates/repo/issue/list.tmpl | 8 +++++++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index 8c173433f..df38e6851 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -1251,6 +1251,8 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { 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 { @@ -1312,13 +1314,17 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) } - if opts.LabelIDs != nil { - for i, labelID := range opts.LabelIDs { - if labelID > 0 { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) - } else { - sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) + if len(opts.LabelIDs) > 0 { + if opts.LabelIDs[0] == 0 { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") + } else { + for i, labelID := range opts.LabelIDs { + if labelID > 0 { + sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), + fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + } else if labelID < 0 { // 0 is not supported here, so just ignore it + sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) + } } } } @@ -1705,17 +1711,21 @@ func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, sess.In("issue.id", issueIDs) } - if len(opts.Labels) > 0 && opts.Labels != "0" { + if len(opts.Labels) > 0 { labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ",")) if err != nil { log.Warn("Malformed Labels argument: %s", opts.Labels) } else { - for i, labelID := range labelIDs { - if labelID > 0 { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) - } else { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label WHERE label_id = ?)", -labelID) + if labelIDs[0] == 0 { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") + } else { + for i, labelID := range labelIDs { + if labelID > 0 { + sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), + fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + } else if labelID < 0 { // 0 is not supported here, so just ignore it + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label WHERE label_id = ?)", -labelID) + } } } } @@ -1734,6 +1744,8 @@ func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, if opts.AssigneeID > 0 { applyAssigneeCondition(sess, opts.AssigneeID) + } else if opts.AssigneeID == db.NoConditionID { + sess.Where("id NOT IN (SELECT issue_id FROM issue_assignees)") } if opts.PosterID > 0 { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2167471aa..78dbb3c9c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1361,6 +1361,7 @@ issues.delete_branch_at = `deleted branch %s %s` issues.filter_label = Label issues.filter_label_exclude = `Use alt + click/enter to exclude labels` issues.filter_label_no_select = All labels +issues.filter_label_select_no_label = No Label issues.filter_milestone = Milestone issues.filter_milestone_all = All milestones issues.filter_milestone_none = No milestones @@ -1371,6 +1372,7 @@ 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_poster = Author issues.filter_poster_no_select = All authors issues.filter_type = Type diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index c2f30a01f..66a498613 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -170,8 +170,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti repo := ctx.Repo.Repository 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 len(selectLabels) > 0 && selectLabels != "0" { + if len(selectLabels) > 0 { labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) if err != nil { ctx.ServerError("StringsToInt64s", err) diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 68d40ffea..7c2f73ca5 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -38,6 +38,7 @@ {{.locale.Tr "repo.issues.filter_label_exclude" | Safe}} + {{.locale.Tr "repo.issues.filter_label_select_no_label"}} {{.locale.Tr "repo.issues.filter_label_no_select"}} {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} @@ -156,6 +157,7 @@ {{svg "octicon-search" 16}} + {{.locale.Tr "repo.issues.filter_assginee_no_assignee"}} {{.locale.Tr "repo.issues.filter_assginee_no_select"}} {{range .Assignees}} @@ -226,6 +228,9 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}}