diff --git a/models/issue.go b/models/issue.go
index 688a412d8c370..f0069178911d0 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1248,8 +1248,12 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
if opts.LabelIDs != nil {
for i, labelID := range opts.LabelIDs {
- 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))
+ 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)
+ }
}
}
}
diff --git a/models/issue_label.go b/models/issue_label.go
index 9efc7fd51f4d4..1fc873cfd44ae 100644
--- a/models/issue_label.go
+++ b/models/issue_label.go
@@ -72,6 +72,7 @@ type Label struct {
IsChecked bool `xorm:"-"`
QueryString string `xorm:"-"`
IsSelected bool `xorm:"-"`
+ IsExcluded bool `xorm:"-"`
}
// APIFormat converts a Label to the api.Label format
@@ -97,7 +98,10 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64)
for _, s := range currentSelectedLabels {
if s == label.ID {
labelSelected = true
- } else if s > 0 {
+ } else if -s == label.ID {
+ labelSelected = true
+ label.IsExcluded = true
+ } else if s != 0 {
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
}
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index eb38a777c82f0..60acab01783c8 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -802,6 +802,7 @@ issues.delete_branch_at = `deleted branch %s %s`
issues.open_tab = %d Open
issues.close_tab = %d Closed
issues.filter_label = Label
+issues.filter_label_exclude = `Use alt
+ click/enter
to exclude labels`
issues.filter_label_no_select = All labels
issues.filter_milestone = Milestone
issues.filter_milestone_no_select = All milestones
diff --git a/public/css/index.css b/public/css/index.css
index 9292604422dd1..e404c1fec636f 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -458,6 +458,8 @@ i.icon.centerlock{top:1.5em}
.repository .filter.menu .label.color{border-radius:3px;margin-left:15px;padding:0 8px}
.repository .filter.menu .octicon{float:left;margin:5px -7px 0 -5px;width:16px}
.repository .filter.menu.labels .octicon{margin:-2px -7px 0 -5px}
+.repository .filter.menu.labels .label-filter .menu .info{display:inline-block;padding:9px 7px 7px 7px;text-align:center;border-bottom:1px solid #ccc;font-size:12px}
+.repository .filter.menu.labels .label-filter .menu .info code{border:1px solid #ccc;border-radius:3px;padding:3px 2px 1px 2px;font-size:11px}
.repository .filter.menu .text{margin-left:.9em}
.repository .filter.menu .menu{max-height:300px;overflow-x:auto;right:0!important;left:auto!important}
.repository .filter.menu .dropdown.item{margin:1px;padding-right:0}
diff --git a/public/js/index.js b/public/js/index.js
index 90819677a5703..cf19bf71a0364 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -3278,8 +3278,39 @@ function initIssueList() {
},
fullTextSearch: true
- })
- ;
+ });
+
+ $(".menu a.label-filter-item").each(function() {
+ $(this).click(function(e) {
+ if (e.altKey) {
+ const href = $(this).attr("href");
+ const id = $(this).data("label-id");
+
+ const regStr = "labels=(-?[0-9]+%2c)*(" + id + ")(%2c-?[0-9]+)*&";
+ const newStr = "labels=$1-$2$3&";
+
+ window.location = href.replace(new RegExp(regStr), newStr);
+ }
+ });
+ });
+
+ $(".menu .ui.dropdown.label-filter").keydown(function(e) {
+ if (e.altKey && e.keyCode == 13) {
+ const selectedItems = $(".menu .ui.dropdown.label-filter .menu .item.selected");
+
+ if (selectedItems.length > 0) {
+ const item = $(selectedItems[0]);
+
+ const href = item.attr("href");
+ const id = item.data("label-id");
+
+ const regStr = "labels=(-?[0-9]+%2c)*(" + id + ")(%2c-?[0-9]+)*&";
+ const newStr = "labels=$1-$2$3&";
+
+ window.location = href.replace(new RegExp(regStr), newStr);
+ }
+ }
+ });
}
function cancelCodeComment(btn) {
const form = $(btn).closest("form");
diff --git a/public/less/_repository.less b/public/less/_repository.less
index 33ee5761c40dc..48a1214c076be 100644
--- a/public/less/_repository.less
+++ b/public/less/_repository.less
@@ -158,6 +158,23 @@
margin: -2px -7px 0 -5px;
}
+ &.labels {
+ .label-filter .menu .info {
+ display: inline-block;
+ padding: 9px 7px 7px 7px;
+ text-align: center;
+ border-bottom: 1px solid #cccccc;
+ font-size: 12px;
+
+ code {
+ border: 1px solid #cccccc;
+ border-radius: 3px;
+ padding: 3px 2px 1px 2px;
+ font-size: 11px;
+ }
+ }
+ }
+
.text {
margin-left: 0.9em;
}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index e64cef2724ce8..9b354a6800bad 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -42,15 +42,16 @@