Commit 7d0703d3 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'master' into '22132-rename-branch-name-params-to-branch-ee'

# Conflicts:
#   doc/api/v3_to_v4.md
parents 6bb74198 8c5d328c
...@@ -9,7 +9,7 @@ variables: ...@@ -9,7 +9,7 @@ variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment # retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3" RSPEC_RETRY_RETRY_COUNT: "3"
ELASTIC_HOST: "registry.gitlab.com-gitlab-org-test-elastic-image" ELASTIC_HOST: "elasticsearch"
RAILS_ENV: "test" RAILS_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
SETUP_DB: "true" SETUP_DB: "true"
...@@ -17,6 +17,8 @@ variables: ...@@ -17,6 +17,8 @@ variables:
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
# This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms600m -Xmx600m"
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
...@@ -55,7 +57,7 @@ stages: ...@@ -55,7 +57,7 @@ stages:
services: services:
- mysql:latest - mysql:latest
- redis:alpine - redis:alpine
- registry.gitlab.com/gitlab-org/test-elastic-image - elasticsearch:5.1
.rspec-knapsack: &rspec-knapsack .rspec-knapsack: &rspec-knapsack
stage: test stage: test
......
...@@ -104,9 +104,10 @@ gem 'unf', '~> 0.1.4' ...@@ -104,9 +104,10 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5' gem 'seed-fu', '~> 2.3.5'
# Search # Search
gem 'elasticsearch-model' gem 'elasticsearch-model', '~> 0.1.9'
gem 'elasticsearch-rails' gem 'elasticsearch-rails', '~> 0.1.9'
gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git" gem 'elasticsearch-api', '5.0.3'
gem 'gitlab-elasticsearch-git', '1.1.1', require: "elasticsearch/git"
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
......
...@@ -86,7 +86,7 @@ GEM ...@@ -86,7 +86,7 @@ GEM
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.4.1) brakeman (3.4.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.2) builder (3.2.3)
bullet (5.2.0) bullet (5.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
...@@ -169,17 +169,17 @@ GEM ...@@ -169,17 +169,17 @@ GEM
railties (>= 4.2) railties (>= 4.2)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
rails (> 3.1) rails (> 3.1)
elasticsearch (1.0.15) elasticsearch (5.0.3)
elasticsearch-api (= 1.0.15) elasticsearch-api (= 5.0.3)
elasticsearch-transport (= 1.0.15) elasticsearch-transport (= 5.0.3)
elasticsearch-api (1.0.15) elasticsearch-api (5.0.3)
multi_json multi_json
elasticsearch-model (0.1.8) elasticsearch-model (0.1.9)
activesupport (> 3) activesupport (> 3)
elasticsearch (> 0.4) elasticsearch (> 0.4)
hashie hashie
elasticsearch-rails (0.1.8) elasticsearch-rails (0.1.9)
elasticsearch-transport (1.0.15) elasticsearch-transport (5.0.3)
faraday faraday
multi_json multi_json
email_reply_trimmer (0.1.6) email_reply_trimmer (0.1.6)
...@@ -265,12 +265,12 @@ GEM ...@@ -265,12 +265,12 @@ GEM
mime-types (>= 1.19) mime-types (>= 1.19)
rugged (>= 0.23.0b) rugged (>= 0.23.0b)
github-markup (1.4.0) github-markup (1.4.0)
gitlab-elasticsearch-git (1.0.1) gitlab-elasticsearch-git (1.1.1)
activemodel (~> 4.2) activemodel (~> 4.2)
activesupport (~> 4.2) activesupport (~> 4.2)
charlock_holmes (~> 0.7) charlock_holmes (~> 0.7)
elasticsearch-api (~> 1.0) elasticsearch-api
elasticsearch-model (~> 0.1.8) elasticsearch-model (~> 0.1.9)
github-linguist (~> 4.7) github-linguist (~> 4.7)
rugged (~> 0.24) rugged (~> 0.24)
gitlab-flowdock-git-hook (1.0.1) gitlab-flowdock-git-hook (1.0.1)
...@@ -353,7 +353,7 @@ GEM ...@@ -353,7 +353,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashie (3.4.4) hashie (3.5.1)
health_check (2.2.1) health_check (2.2.1)
rails (>= 4.0) rails (>= 4.0)
hipchat (1.5.2) hipchat (1.5.2)
...@@ -889,8 +889,9 @@ DEPENDENCIES ...@@ -889,8 +889,9 @@ DEPENDENCIES
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.2.0) doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
elasticsearch-model elasticsearch-api (= 5.0.3)
elasticsearch-rails elasticsearch-model (~> 0.1.9)
elasticsearch-rails (~> 0.1.9)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
...@@ -908,7 +909,7 @@ DEPENDENCIES ...@@ -908,7 +909,7 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-elasticsearch-git (~> 1.0.1) gitlab-elasticsearch-git (= 1.1.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
......
...@@ -76,7 +76,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -76,7 +76,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager) { if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager(); new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
} }
Issuable.init(); Issuable.init();
new gl.IssuableBulkActions({ new gl.IssuableBulkActions({
......
...@@ -37,27 +37,18 @@ require('./filtered_search_dropdown'); ...@@ -37,27 +37,18 @@ require('./filtered_search_dropdown');
} }
renderContent() { renderContent() {
const dropdownData = [{ const dropdownData = [];
icon: 'fa-pencil',
hint: 'author:', [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
tag: '<@author>', const { icon, hint, tag } = dropdownMenu.dataset;
}, { if (icon && hint && tag) {
icon: 'fa-user', dropdownData.push({
hint: 'assignee:', icon: `fa-${icon}`,
tag: '<@assignee>', hint,
}, { tag: `<${tag}>`,
icon: 'fa-clock-o', });
hint: 'milestone:', }
tag: '<%milestone>', });
}, {
icon: 'fa-tag',
hint: 'label:',
tag: '<~label>',
}, {
icon: 'fa-balance-scale',
hint: 'weight:',
tag: '<weight>',
}];
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
......
...@@ -52,8 +52,9 @@ ...@@ -52,8 +52,9 @@
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
if (forceShowList && this.getCurrentHook().list.hidden) { const currentHook = this.getCurrentHook();
this.getCurrentHook().list.show(); if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show();
} }
} }
...@@ -92,18 +93,23 @@ ...@@ -92,18 +93,23 @@
} }
hideDropdown() { hideDropdown() {
this.getCurrentHook().list.hide(); const currentHook = this.getCurrentHook();
if (currentHook) {
currentHook.list.hide();
}
} }
resetFilters() { resetFilters() {
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
const data = hook.list.data; if (hook) {
const results = data.map((o) => { const data = hook.list.data;
const updated = o; const results = data.map((o) => {
updated.droplab_hidden = false; const updated = o;
return updated; updated.droplab_hidden = false;
}); return updated;
hook.list.render(results); });
hook.list.render(results);
}
} }
} }
......
...@@ -2,10 +2,16 @@ ...@@ -2,10 +2,16 @@
(() => { (() => {
class FilteredSearchDropdownManager { class FilteredSearchDropdownManager {
constructor(baseEndpoint = '') { constructor(baseEndpoint = '', page) {
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer; this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchInput = document.querySelector('.filtered-search'); this.filteredSearchInput = document.querySelector('.filtered-search');
this.page = page;
if (this.page === 'issues') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights;
}
this.setupMapping(); this.setupMapping();
...@@ -48,17 +54,20 @@ ...@@ -48,17 +54,20 @@
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: document.querySelector('#js-dropdown-label'), element: document.querySelector('#js-dropdown-label'),
}, },
weight: {
reference: null,
gl: 'DropdownNonUser',
element: document.querySelector('#js-dropdown-weight'),
},
hint: { hint: {
reference: null, reference: null,
gl: 'DropdownHint', gl: 'DropdownHint',
element: document.querySelector('#js-dropdown-hint'), element: document.querySelector('#js-dropdown-hint'),
}, },
}; };
if (this.page === 'issues') {
this.mapping.weight = {
reference: null,
gl: 'DropdownNonUser',
element: document.querySelector('#js-dropdown-weight'),
};
}
} }
static addWordToInput(tokenName, tokenValue = '') { static addWordToInput(tokenName, tokenValue = '') {
...@@ -155,7 +164,7 @@ ...@@ -155,7 +164,7 @@
this.droplab = new DropLab(); this.droplab = new DropLab();
} }
const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key]; && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
......
(() => { (() => {
class FilteredSearchManager { class FilteredSearchManager {
constructor() { constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search'); this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search'); this.clearSearchButton = document.querySelector('.clear-search');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (page === 'issues') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights;
}
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer; this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || ''); this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
this.bindEvents(); this.bindEvents();
this.loadSearchParamsFromURL(); this.loadSearchParamsFromURL();
...@@ -118,7 +123,7 @@ ...@@ -118,7 +123,7 @@
const value = split[1]; const value = split[1];
// Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) { if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`); inputValues.push(`${condition.tokenKey}:${condition.value}`);
...@@ -126,7 +131,7 @@ ...@@ -126,7 +131,7 @@
// Sanitize value since URL converts spaces into + // Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Replace before decode so that we know what was originally + versus the encoded +
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) { if (match) {
const indexOf = keyParam.indexOf('_'); const indexOf = keyParam.indexOf('_');
...@@ -171,9 +176,9 @@ ...@@ -171,9 +176,9 @@
paths.push(`state=${currentState}`); paths.push(`state=${currentState}`);
tokens.forEach((token) => { tokens.forEach((token) => {
const condition = gl.FilteredSearchTokenKeys const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase()); .searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key) || {}; const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const keyParam = param ? `${token.key}_${param}` : token.key; const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = ''; let tokenPath = '';
......
...@@ -19,11 +19,6 @@ ...@@ -19,11 +19,6 @@
type: 'array', type: 'array',
param: 'name[]', param: 'name[]',
symbol: '~', symbol: '~',
}, {
key: 'weight',
type: 'string',
param: '',
symbol: '',
}]; }];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
...@@ -51,14 +46,6 @@ ...@@ -51,14 +46,6 @@
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
}, {
url: 'weight=No+Weight',
tokenKey: 'weight',
value: 'none',
}, {
url: 'weight=Any+Weight',
tokenKey: 'weight',
value: 'any',
}]; }];
class FilteredSearchTokenKeys { class FilteredSearchTokenKeys {
......
require('./filtered_search_token_keys');
const weightTokenKey = {
key: 'weight',
type: 'string',
param: '',
symbol: '',
};
const weightConditions = [{
url: 'weight=No+Weight',
tokenKey: 'weight',
value: 'none',
}, {
url: 'weight=Any+Weight',
tokenKey: 'weight',
value: 'any',
}];
class FilteredSearchTokenKeysWithWeights extends gl.FilteredSearchTokenKeys {
static get() {
const tokenKeys = super.get();
tokenKeys.push(weightTokenKey);
return tokenKeys;
}
static getAlternatives() {
return super.getAlternatives();
}
static getConditions() {
const conditions = super.getConditions();
return conditions.concat(weightConditions);
}
static searchByKey(key) {
const tokenKeys = FilteredSearchTokenKeysWithWeights.get();
return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
}
static searchBySymbol(symbol) {
const tokenKeys = FilteredSearchTokenKeysWithWeights.get();
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
}
static searchByKeyParam(keyParam) {
const tokenKeys = FilteredSearchTokenKeysWithWeights.get();
const alternativeTokenKeys = FilteredSearchTokenKeysWithWeights.getAlternatives();
const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`;
}
return keyParam === tokenKeyParam;
}) || null;
}
static searchByConditionUrl(url) {
const conditions = FilteredSearchTokenKeysWithWeights.getConditions();
return conditions.find(condition => condition.url === url) || null;
}
static searchByConditionKeyValue(key, value) {
const conditions = FilteredSearchTokenKeysWithWeights.getConditions();
return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null;
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenKeysWithWeights = FilteredSearchTokenKeysWithWeights;
...@@ -169,10 +169,10 @@ ...@@ -169,10 +169,10 @@
url: issuesPath + "/?author_username=" + userName url: issuesPath + "/?author_username=" + userName
}, 'separator', { }, 'separator', {
text: 'Merge requests assigned to me', text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId url: mrPath + "/?assignee_username=" + userName
}, { }, {
text: "Merge requests I've created", text: "Merge requests I've created",
url: mrPath + "/?author_id=" + userId url: mrPath + "/?author_username=" + userName
} }
]; ];
if (!name) { if (!name) {
......
...@@ -53,6 +53,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -53,6 +53,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, labels_params).execute @labels = LabelsFinder.new(current_user, labels_params).execute
end end
@users = []
if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
@users.push(author) if author
end
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
......
...@@ -7,14 +7,14 @@ module Elastic ...@@ -7,14 +7,14 @@ module Elastic
mappings _parent: { type: 'project' } do mappings _parent: { type: 'project' } do
indexes :id, type: :integer indexes :id, type: :integer
indexes :iid, type: :integer, index: :not_analyzed indexes :iid, type: :integer
indexes :title, type: :string, indexes :title, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :description, type: :string, indexes :description, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :created_at, type: :date indexes :created_at, type: :date
indexes :updated_at, type: :date indexes :updated_at, type: :date
indexes :state, type: :string indexes :state, type: :text
indexes :project_id, type: :integer indexes :project_id, type: :integer
indexes :author_id, type: :integer indexes :author_id, type: :integer
indexes :assignee_id, type: :integer indexes :assignee_id, type: :integer
......
...@@ -8,18 +8,18 @@ module Elastic ...@@ -8,18 +8,18 @@ module Elastic
mappings _parent: { type: 'project' } do mappings _parent: { type: 'project' } do
indexes :id, type: :integer indexes :id, type: :integer
indexes :iid, type: :integer indexes :iid, type: :integer
indexes :target_branch, type: :string, indexes :target_branch, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :source_branch, type: :string, indexes :source_branch, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :title, type: :string, indexes :title, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :description, type: :string, indexes :description, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :created_at, type: :date indexes :created_at, type: :date
indexes :updated_at, type: :date indexes :updated_at, type: :date
indexes :state, type: :string indexes :state, type: :text
indexes :merge_status, type: :string indexes :merge_status, type: :text
indexes :source_project_id, type: :integer indexes :source_project_id, type: :integer
indexes :target_project_id, type: :integer indexes :target_project_id, type: :integer
indexes :author_id, type: :integer indexes :author_id, type: :integer
......
...@@ -7,9 +7,9 @@ module Elastic ...@@ -7,9 +7,9 @@ module Elastic
mappings _parent: { type: 'project' } do mappings _parent: { type: 'project' } do
indexes :id, type: :integer indexes :id, type: :integer
indexes :title, type: :string, indexes :title, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :description, type: :string, indexes :description, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :project_id, type: :integer indexes :project_id, type: :integer
indexes :created_at, type: :date indexes :created_at, type: :date
......
...@@ -7,7 +7,7 @@ module Elastic ...@@ -7,7 +7,7 @@ module Elastic
mappings _parent: { type: 'project' } do mappings _parent: { type: 'project' } do
indexes :id, type: :integer indexes :id, type: :integer
indexes :note, type: :string, indexes :note, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :project_id, type: :integer indexes :project_id, type: :integer
indexes :created_at, type: :date indexes :created_at, type: :date
......
...@@ -7,16 +7,16 @@ module Elastic ...@@ -7,16 +7,16 @@ module Elastic
mappings do mappings do
indexes :id, type: :integer indexes :id, type: :integer
indexes :name, type: :string, indexes :name, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :path, type: :string, indexes :path, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :name_with_namespace, type: :string, indexes :name_with_namespace, type: :text,
index_options: 'offsets', index_options: 'offsets',
analyzer: :my_ngram_analyzer analyzer: :my_ngram_analyzer
indexes :path_with_namespace, type: :string, indexes :path_with_namespace, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :description, type: :string, indexes :description, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :namespace_id, type: :integer indexes :namespace_id, type: :integer
indexes :created_at, type: :date indexes :created_at, type: :date
...@@ -91,7 +91,7 @@ module Elastic ...@@ -91,7 +91,7 @@ module Elastic
} }
end end
query_hash[:query][:bool][:filter] = { and: filters } query_hash[:query][:bool][:filter] = filters
query_hash[:sort] = [:_score] query_hash[:sort] = [:_score]
......
...@@ -7,15 +7,15 @@ module Elastic ...@@ -7,15 +7,15 @@ module Elastic
mappings do mappings do
indexes :id, type: :integer indexes :id, type: :integer
indexes :title, type: :string, indexes :title, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :file_name, type: :string, indexes :file_name, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :content, type: :string, indexes :content, type: :text,
index_options: 'offsets' index_options: 'offsets'
indexes :created_at, type: :date indexes :created_at, type: :date
indexes :updated_at, type: :date indexes :updated_at, type: :date
indexes :state, type: :string indexes :state, type: :text
indexes :project_id, type: :integer indexes :project_id, type: :integer
indexes :author_id, type: :integer indexes :author_id, type: :integer
indexes :visibility_level, type: :integer indexes :visibility_level, type: :integer
......
...@@ -5,18 +5,19 @@ ...@@ -5,18 +5,19 @@
= render "projects/issues/head" = render "projects/issues/head"
= render 'projects/last_push' = render 'projects/last_push'
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search')
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .nav-controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project - if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
New Merge Request New Merge Request
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
{{hint}} {{hint}}
%span.js-filter-tag.dropdown-light-content %span.js-filter-tag.dropdown-light-content
{{tag}} {{tag}}
#js-dropdown-author.dropdown-menu #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.dropdown-user %button.btn.btn-link.dropdown-user
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
{{name}} {{name}}
%span.dropdown-light-content %span.dropdown-light-content
@{{username}} @{{username}}
#js-dropdown-assignee.dropdown-menu #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
{{name}} {{name}}
%span.dropdown-light-content %span.dropdown-light-content
@{{username}} @{{username}}
#js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.js-data-value %button.btn.btn-link.js-data-value
{{title}} {{title}}
#js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link %button.btn.btn-link
...@@ -82,19 +82,20 @@ ...@@ -82,19 +82,20 @@
%span.dropdown-label-box{ style: 'background: {{color}}' } %span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value %span.label-title.js-data-value
{{title}} {{title}}
#js-dropdown-weight.dropdown-menu{ 'data-dropdown' => true } - if type == :issues
%ul{ 'data-dropdown' => true } #js-dropdown-weight.dropdown-menu{ data: { icon: 'balance-scale', hint: 'weight', tag: 'weight' } }
%li.filter-dropdown-item{ 'data-value' => 'none' } %ul{ 'data-dropdown' => true }
%button.btn.btn-link %li.filter-dropdown-item{ 'data-value' => 'none' }
No Weight %button.btn.btn-link
%li.filter-dropdown-item{ 'data-value' => 'any' } No Weight
%button.btn.btn-link %li.filter-dropdown-item{ 'data-value' => 'any' }
Any Weight %button.btn.btn-link
%li.divider Any Weight
%ul.filter-dropdown{ 'data-dropdown' => true } %li.divider
- Issue.weight_filter_options.each do |weight| %ul.filter-dropdown{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => "#{weight}" } - Issue.weight_filter_options.each do |weight|
%button.btn.btn-link= weight %li.filter-dropdown-item{ 'data-value' => "#{weight}" }
%button.btn.btn-link= weight
.pull-right .pull-right
= render 'shared/sort_dropdown', type: local_assigns[:type] = render 'shared/sort_dropdown', type: local_assigns[:type]
......
...@@ -36,7 +36,7 @@ class ElasticIndexerWorker ...@@ -36,7 +36,7 @@ class ElasticIndexerWorker
client.delete index: klass.index_name, type: klass.document_type, id: record_id client.delete index: klass.index_name, type: klass.document_type, id: record_id
end end
clear_project_indexes(record_id) if klass == Project clear_project_data(record_id) if klass == Project
end end
rescue Elasticsearch::Transport::Transport::Errors::NotFound, ActiveRecord::RecordNotFound rescue Elasticsearch::Transport::Transport::Errors::NotFound, ActiveRecord::RecordNotFound
# These errors can happen in several cases, including: # These errors can happen in several cases, including:
...@@ -56,43 +56,33 @@ class ElasticIndexerWorker ...@@ -56,43 +56,33 @@ class ElasticIndexerWorker
end end
end end
def clear_project_indexes(record_id) def clear_project_data(record_id)
remove_repository_index(record_id) remove_children_documents(Repository.document_type, record_id)
remove_wiki_index(record_id) remove_children_documents(ProjectWiki.document_type, record_id)
remove_nested_content(record_id) remove_children_documents(MergeRequest.document_type, record_id)
remove_documents_by_project_id(record_id)
end end
def remove_repository_index(record_id) def remove_documents_by_project_id(record_id)
client.delete_by_query({
index: Repository.__elasticsearch__.index_name,
body: {
query: {
or: [
{ term: { "commit.rid" => record_id } },
{ term: { "blob.rid" => record_id } }
]
}
}
})
end
def remove_nested_content(record_id)
client.delete_by_query({ client.delete_by_query({
index: Project.__elasticsearch__.index_name, index: Project.__elasticsearch__.index_name,
body: { body: {
query: { query: {
term: { "_parent" => record_id } term: { "project_id" => record_id }
} }
} }
}) })
end end
def remove_wiki_index(record_id) def remove_children_documents(document_type, parent_record_id)
client.delete_by_query({ client.delete_by_query({
index: ProjectWiki.__elasticsearch__.index_name, index: Project.__elasticsearch__.index_name,
body: { body: {
query: { query: {
term: { "blob.rid" => "wiki_#{record_id}" } parent_id: {
type: document_type,
id: parent_record_id
}
} }
} }
}) })
......
---
title: Add filtered search to MR page
merge_request: 1243
author:
---
title: 'API: Use `post ":id/#{type}/:subscribable_id/subscribe"` to subscribe and
`post ":id/#{type}/:subscribable_id/unsubscribe"` to unsubscribe from a resource'
merge_request: 1274
author: Robert Schilling
---
title: Update Elasticsearch to 5.1
merge_request:
author:
...@@ -523,7 +523,7 @@ If the user is already subscribed to the issue, the status code `304` ...@@ -523,7 +523,7 @@ If the user is already subscribed to the issue, the status code `304`
is returned. is returned.
``` ```
POST /projects/:id/issues/:issue_id/subscription POST /projects/:id/issues/:issue_id/subscribe
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -532,7 +532,7 @@ POST /projects/:id/issues/:issue_id/subscription ...@@ -532,7 +532,7 @@ POST /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue | | `issue_id` | integer | yes | The ID of a project's issue |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe
``` ```
Example response: Example response:
...@@ -579,7 +579,7 @@ from it. If the user is not subscribed to the issue, the ...@@ -579,7 +579,7 @@ from it. If the user is not subscribed to the issue, the
status code `304` is returned. status code `304` is returned.
``` ```
DELETE /projects/:id/issues/:issue_id/subscription POST /projects/:id/issues/:issue_id/unsubscribe
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -588,7 +588,7 @@ DELETE /projects/:id/issues/:issue_id/subscription ...@@ -588,7 +588,7 @@ DELETE /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue | | `issue_id` | integer | yes | The ID of a project's issue |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe
``` ```
Example response: Example response:
......
...@@ -188,12 +188,12 @@ Example response: ...@@ -188,12 +188,12 @@ Example response:
## Subscribe to a label ## Subscribe to a label
Subscribes the authenticated user to a label to receive notifications. Subscribes the authenticated user to a label to receive notifications.
If the user is already subscribed to the label, the status code `304` If the user is already subscribed to the label, the status code `304`
is returned. is returned.
``` ```
POST /projects/:id/labels/:label_id/subscription POST /projects/:id/labels/:label_id/subscribe
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -202,7 +202,7 @@ POST /projects/:id/labels/:label_id/subscription ...@@ -202,7 +202,7 @@ POST /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label | | `label_id` | integer or string | yes | The ID or title of a project's label |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscribe
``` ```
Example response: Example response:
...@@ -228,7 +228,7 @@ from it. If the user is not subscribed to the label, the ...@@ -228,7 +228,7 @@ from it. If the user is not subscribed to the label, the
status code `304` is returned. status code `304` is returned.
``` ```
DELETE /projects/:id/labels/:label_id/subscription POST /projects/:id/labels/:label_id/unsubscribe
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -237,7 +237,7 @@ DELETE /projects/:id/labels/:label_id/subscription ...@@ -237,7 +237,7 @@ DELETE /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label | | `label_id` | integer or string | yes | The ID or title of a project's label |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/unsubscribe
``` ```
Example response: Example response:
......
...@@ -810,7 +810,7 @@ Subscribes the authenticated user to a merge request to receive notification. If ...@@ -810,7 +810,7 @@ Subscribes the authenticated user to a merge request to receive notification. If
status code `304` is returned. status code `304` is returned.
``` ```
POST /projects/:id/merge_requests/:merge_request_id/subscription POST /projects/:id/merge_requests/:merge_request_id/subscribe
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -819,7 +819,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription ...@@ -819,7 +819,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request | | `merge_request_id` | integer | yes | The ID of the merge request |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe
``` ```
Example response: Example response:
...@@ -884,7 +884,7 @@ notifications from that merge request. If the user is ...@@ -884,7 +884,7 @@ notifications from that merge request. If the user is
not subscribed to the merge request, the status code `304` is returned. not subscribed to the merge request, the status code `304` is returned.
``` ```
DELETE /projects/:id/merge_requests/:merge_request_id/subscription POST /projects/:id/merge_requests/:merge_request_id/subscribe
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -893,7 +893,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription ...@@ -893,7 +893,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request | | `merge_request_id` | integer | yes | The ID of the merge request |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe
``` ```
Example response: Example response:
......
...@@ -28,10 +28,10 @@ changes are in V4: ...@@ -28,10 +28,10 @@ changes are in V4:
- Return pagination headers for all endpoints that return an array - Return pagination headers for all endpoints that return an array
- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead - Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead
- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` - Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)`
- Make subscription API more RESTful. Use `post ":id/#{type}/:subscribable_id/subscribe"` to subscribe and `post ":id/#{type}/:subscribable_id/unsubscribe"` to unsubscribe from a resource.
- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) - Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR)
- Renamed param `branch_name` to `branch` on the following endpoints - Renamed param `branch_name` to `branch` on the following endpoints
- POST `:id/repository/branches` - POST `:id/repository/branches`
- POST `:id/repository/commits` - POST `:id/repository/commits`
- POST/PUT/DELETE `:id/repository/files` - POST/PUT/DELETE `:id/repository/files`
- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response
...@@ -29,12 +29,7 @@ GitLab, or on a separate server. ...@@ -29,12 +29,7 @@ GitLab, or on a separate server.
## Requirements ## Requirements
These are the requirements needed for Elasticsearch to work: Elasticsearch 5.1.x.
- GitLab 8.4+
- Elasticsearch 2.4.x (with [Delete By Query Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/2.4/plugins-delete-by-query.html) installed)
Please note that we don't support Elasticsearch 5.x at this time.
## Install Elasticsearch ## Install Elasticsearch
......
...@@ -299,13 +299,6 @@ Feature: Project Merge Requests ...@@ -299,13 +299,6 @@ Feature: Project Merge Requests
And I preview a description text like "Bug fixed :smile:" And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab Then I should see the Markdown write tab
@javascript
Scenario: I search merge request
Given I click link "All"
When I fill in merge request search with "Fe"
Then I should see "Feature NS-03" in merge requests
And I should not see "Bug NS-04" in merge requests
@javascript @javascript
Scenario: I can unsubscribe from merge request Scenario: I can unsubscribe from merge request
Given I visit merge request page "Bug NS-04" Given I visit merge request page "Bug NS-04"
......
...@@ -19,6 +19,7 @@ module API ...@@ -19,6 +19,7 @@ module API
mount ::API::V3::Projects mount ::API::V3::Projects
mount ::API::V3::ProjectSnippets mount ::API::V3::ProjectSnippets
mount ::API::V3::Repositories mount ::API::V3::Repositories
mount ::API::V3::Subscriptions
mount ::API::V3::SystemHooks mount ::API::V3::SystemHooks
mount ::API::V3::Tags mount ::API::V3::Tags
mount ::API::V3::Templates mount ::API::V3::Templates
......
...@@ -21,7 +21,7 @@ module API ...@@ -21,7 +21,7 @@ module API
desc 'Subscribe to a resource' do desc 'Subscribe to a resource' do
success entity_class success entity_class
end end
post ":id/#{type}/:subscribable_id/subscription" do post ":id/#{type}/:subscribable_id/subscribe" do
resource = instance_exec(params[:subscribable_id], &finder) resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project) if resource.subscribed?(current_user, user_project)
...@@ -35,7 +35,7 @@ module API ...@@ -35,7 +35,7 @@ module API
desc 'Unsubscribe from a resource' do desc 'Unsubscribe from a resource' do
success entity_class success entity_class
end end
delete ":id/#{type}/:subscribable_id/subscription" do post ":id/#{type}/:subscribable_id/unsubscribe" do
resource = instance_exec(params[:subscribable_id], &finder) resource = instance_exec(params[:subscribable_id], &finder)
if !resource.subscribed?(current_user, user_project) if !resource.subscribed?(current_user, user_project)
......
module API
module V3
class Subscriptions < Grape::API
before { authenticate! }
subscribable_types = {
'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) },
}
params do
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
resource :projects do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = ::API::Entities.const_get(type_singularized.camelcase)
desc 'Subscribe to a resource' do
success entity_class
end
post ":id/#{type}/:subscribable_id/subscription" do
resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project)
not_modified!
else
resource.subscribe(current_user, user_project)
present resource, with: entity_class, current_user: current_user, project: user_project
end
end
desc 'Unsubscribe from a resource' do
success entity_class
end
delete ":id/#{type}/:subscribable_id/subscription" do
resource = instance_exec(params[:subscribable_id], &finder)
if !resource.subscribed?(current_user, user_project)
not_modified!
else
resource.unsubscribe(current_user, user_project)
present resource, with: entity_class, current_user: current_user, project: user_project
end
end
end
end
end
end
end
...@@ -1082,23 +1082,15 @@ namespace :gitlab do ...@@ -1082,23 +1082,15 @@ namespace :gitlab do
client = Elasticsearch::Client.new(host: ApplicationSetting.current.elasticsearch_host, client = Elasticsearch::Client.new(host: ApplicationSetting.current.elasticsearch_host,
port: ApplicationSetting.current.elasticsearch_port) port: ApplicationSetting.current.elasticsearch_port)
print "Elasticsearch version 2.4.x? ... " print "Elasticsearch version 5.1.x? ... "
version = Gitlab::VersionInfo.parse(client.info["version"]["number"]) version = Gitlab::VersionInfo.parse(client.info["version"]["number"])
if version.major == 2 && version.minor == 4 if version.major == 5 && version.minor == 1
puts "yes (#{version})".color(:green) puts "yes (#{version})".color(:green)
else else
puts "no, you have #{version}".color(:red) puts "no, you have #{version}".color(:red)
end end
print "Elasticsearch has plugin delete-by-query installed? ... "
if client.cat.plugins.include?("delete-by-query")
puts "yes".color(:green)
else
puts "no".color(:red)
end
end end
def check_gitlab_geo_node(node) def check_gitlab_geo_node(node)
......
require 'spec_helper' require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do describe 'Dropdown label', js: true, feature: true do
include FilteredSearchHelpers
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') } let(:filtered_search) { find('.filtered-search') }
...@@ -17,12 +19,6 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -17,12 +19,6 @@ describe 'Dropdown label', js: true, feature: true do
let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') } let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') }
end end
def init_label_search
filtered_search.set('label:')
# This ensures the dropdown is shown
expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
def search_for_label(label) def search_for_label(label)
init_label_search init_label_search
filtered_search.send_keys(label) filtered_search.send_keys(label)
......
require 'rails_helper' require 'rails_helper'
describe 'Filter issues', js: true, feature: true do describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:group) { create(:group) } let!(:group) { create(:group) }
...@@ -17,19 +18,6 @@ describe 'Filter issues', js: true, feature: true do ...@@ -17,19 +18,6 @@ describe 'Filter issues', js: true, feature: true do
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
let(:filtered_search) { find('.filtered-search') }
def input_filtered_search(search_term, submit: true)
filtered_search.set(search_term)
if submit
filtered_search.send_keys(:enter)
end
end
def expect_filtered_search_input(input)
expect(find('.filtered-search').value).to eq(input)
end
def expect_no_issues_list def expect_no_issues_list
page.within '.issues-list' do page.within '.issues-list' do
......
require 'rails_helper' require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do feature 'Issue filtering by Labels', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax include WaitForAjax
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
...@@ -32,123 +34,77 @@ feature 'Issue filtering by Labels', feature: true, js: true do ...@@ -32,123 +34,77 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context 'filter by label bug' do context 'filter by label bug' do
before do before do
select_labels('bug') input_filtered_search('label:~bug')
end end
it 'apply the filter' do it 'apply the filter' do
expect(page).to have_content "Bugfix1" expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end end
end end
context 'filter by label feature' do context 'filter by label feature' do
before do before do
select_labels('feature') input_filtered_search('label:~feature')
end end
it 'applies the filter' do it 'applies the filter' do
expect(page).to have_content "Feature1" expect(page).to have_content "Feature1"
expect(page).not_to have_content "Bugfix2" expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end end
end end
context 'filter by label enhancement' do context 'filter by label enhancement' do
before do before do
select_labels('enhancement') input_filtered_search('label:~enhancement')
end end
it 'applies the filter' do it 'applies the filter' do
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'filter by label enhancement and bug in issues list' do context 'filter by label enhancement and bug in issues list' do
before do before do
select_labels('bug', 'enhancement') input_filtered_search('label:~bug label:~enhancement')
end end
it 'applies the filters' do it 'applies the filters' do
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).not_to have_content "feature"
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'remove filtered labels' do context 'clear button' do
before do before do
page.within '.labels-filter' do input_filtered_search('label:~bug')
click_button 'Label'
wait_for_ajax
click_link 'bug'
find('.dropdown-menu-close').click
end
page.within '.filtered-labels' do
expect(page).to have_content 'bug'
end
end end
it 'allows user to remove filtered labels' do it 'allows user to remove filtered labels' do
first('.js-label-filter-remove').click first('.clear-search').click
wait_for_ajax filtered_search.send_keys(:enter)
expect(find('.filtered-labels', visible: false)).not_to have_content 'bug' expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
expect(find('.labels-filter')).not_to have_content 'bug' expect(page).to have_content "Bugfix2"
expect(page).to have_content "Feature1"
expect(page).to have_content "Bugfix1"
end end
end end
context 'dropdown filtering' do context 'filter dropdown' do
it 'filters by label name' do it 'filters by label name' do
page.within '.labels-filter' do init_label_search
click_button 'Label' filtered_search.send_keys('~bug')
wait_for_ajax
find('.dropdown-input input').set 'bug'
page.within '.dropdown-content' do
expect(page).not_to have_content 'enhancement'
expect(page).to have_content 'bug'
end
end
end
end
def select_labels(*labels) page.within '.filter-dropdown' do
page.find('.js-label-select').click expect(page).not_to have_content 'enhancement'
wait_for_ajax expect(page).to have_content 'bug'
labels.each do |label| end
execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
end end
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end end
end end
require 'rails_helper' require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do feature 'Merge Request filtering by Milestone', feature: true do
include FilteredSearchHelpers
include MergeRequestHelpers
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
def filter_by_milestone(title)
find(".js-milestone-select").click
find(".milestone-filter a", text: title).click
end
before do before do
project.team << [user, :master] project.team << [user, :master]
login_as(user) login_as(user)
...@@ -15,42 +23,42 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -15,42 +23,42 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::None.title) input_filtered_search('milestone:none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
context 'filters by upcoming milestone', js: true do context 'filters by upcoming milestone', js: true do
it 'does not show issues with no expiry' do it 'does not show merge requests with no expiry' do
create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title) input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0) expect(page).to have_css('.merge-request', count: 0)
end end
it 'shows issues in future' do it 'shows merge requests in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow) milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title) input_filtered_search('milestone:upcoming')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
it 'does not show issues in past' do it 'does not show merge requests in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday) milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title) input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0) expect(page).to have_css('.merge-request', count: 0)
end end
...@@ -61,7 +69,7 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -61,7 +69,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project) create(:merge_request, :simple, source_project: project)
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(milestone.title) input_filtered_search("milestone:%'#{milestone.title}'")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
...@@ -77,19 +85,10 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -77,19 +85,10 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project) create(:merge_request, :simple, source_project: project)
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(milestone.title) input_filtered_search("milestone:%\"#{milestone.title}\"")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
end end
def visit_merge_requests(project)
visit namespace_project_merge_requests_path(project.namespace, project)
end
def filter_by_milestone(title)
find(".js-milestone-select").click
find(".milestone-filter a", text: title).click
end
end end
require 'rails_helper' require 'rails_helper'
feature 'Issues filter reset button', feature: true, js: true do feature 'Issues filter reset button', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax include WaitForAjax
include IssueHelpers include IssueHelpers
let!(:project) { create(:project, :public) } let!(:project) { create(:project, :public) }
let!(:user) { create(:user)} let!(:user) { create(:user) }
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
let!(:bug) { create(:label, project: project, name: 'bug')} let!(:bug) { create(:label, project: project, name: 'bug')}
let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) }
let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") }
let(:merge_request_css) { '.merge-request' } let(:merge_request_css) { '.merge-request' }
let(:clear_search_css) { '.filtered-search-input-container .clear-search' }
before do before do
mr2.labels << bug mr2.labels << bug
...@@ -50,7 +53,7 @@ feature 'Issues filter reset button', feature: true, js: true do ...@@ -50,7 +53,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when author filter has been applied' do context 'when author filter has been applied' do
it 'resets the author filter' do it 'resets the author filter' do
visit_merge_requests(project, author_id: user.id) visit_merge_requests(project, author_username: user.username)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
reset_filters reset_filters
...@@ -60,7 +63,7 @@ feature 'Issues filter reset button', feature: true, js: true do ...@@ -60,7 +63,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when assignee filter has been applied' do context 'when assignee filter has been applied' do
it 'resets the assignee filter' do it 'resets the assignee filter' do
visit_merge_requests(project, assignee_id: user.id) visit_merge_requests(project, assignee_username: user.username)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
reset_filters reset_filters
...@@ -70,7 +73,7 @@ feature 'Issues filter reset button', feature: true, js: true do ...@@ -70,7 +73,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when all filters have been applied' do context 'when all filters have been applied' do
it 'resets all filters' do it 'resets all filters' do
visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 0) expect(page).to have_css(merge_request_css, count: 0)
reset_filters reset_filters
...@@ -82,15 +85,7 @@ feature 'Issues filter reset button', feature: true, js: true do ...@@ -82,15 +85,7 @@ feature 'Issues filter reset button', feature: true, js: true do
it 'the reset link should not be visible' do it 'the reset link should not be visible' do
visit_merge_requests(project) visit_merge_requests(project)
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(page).not_to have_css '.reset_filters' expect(page).not_to have_css(clear_search_css)
end end
end end
def visit_merge_requests(project, opts = {})
visit namespace_project_merge_requests_path project.namespace, project, opts
end
def reset_filters
find('.reset-filters').click
end
end end
...@@ -186,7 +186,7 @@ describe "Search", feature: true do ...@@ -186,7 +186,7 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.merge-requests-holder') expect(page).to have_selector('.merge-requests-holder')
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
end end
it 'takes user to her MR page when MR authored is clicked' do it 'takes user to her MR page when MR authored is clicked' do
...@@ -194,7 +194,7 @@ describe "Search", feature: true do ...@@ -194,7 +194,7 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.merge-requests-holder') expect(page).to have_selector('.merge-requests-holder')
expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) expect(find('.filtered-search').value).to eq("author:@#{user.username}")
end end
end end
......
require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys_with_weights');
(() => {
describe('Filtered Search Token Keys With Weights', () => {
const weightTokenKey = {
key: 'weight',
type: 'string',
param: '',
symbol: '',
};
describe('get', () => {
let tokenKeys;
beforeEach(() => {
tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
});
it('should return tokenKeys', () => {
expect(tokenKeys !== null).toBe(true);
});
it('should return tokenKeys as an array', () => {
expect(tokenKeys instanceof Array).toBe(true);
});
it('should return weightTokenKey as part of tokenKeys', () => {
const match = tokenKeys.find(tk => tk.key === weightTokenKey.key);
expect(match).toEqual(weightTokenKey);
});
});
describe('getConditions', () => {
let conditions;
beforeEach(() => {
conditions = gl.FilteredSearchTokenKeysWithWeights.getConditions();
});
it('should return conditions', () => {
expect(conditions !== null).toBe(true);
});
it('should return conditions as an array', () => {
expect(conditions instanceof Array).toBe(true);
});
it('should return weightConditions as part of conditions', () => {
const weightConditions = conditions.filter(c => c.tokenKey === 'weight');
expect(weightConditions.length).toBe(2);
});
});
describe('searchByKey', () => {
it('should return null when key not found', () => {
const tokenKey = gl.FilteredSearchTokenKeysWithWeights.searchByKey('notakey');
expect(tokenKey === null).toBe(true);
});
it('should return tokenKey when found by key', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
const result = gl.FilteredSearchTokenKeysWithWeights.searchByKey(tokenKeys[0].key);
expect(result).toEqual(tokenKeys[0]);
});
it('should return weight tokenKey when found by weight key', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
const match = tokenKeys.find(tk => tk.key === weightTokenKey.key);
const result = gl.FilteredSearchTokenKeysWithWeights.searchByKey(weightTokenKey.key);
expect(result).toEqual(match);
});
});
describe('searchBySymbol', () => {
it('should return null when symbol not found', () => {
const tokenKey = gl.FilteredSearchTokenKeysWithWeights.searchBySymbol('notasymbol');
expect(tokenKey === null).toBe(true);
});
it('should return tokenKey when found by symbol', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
const result = gl.FilteredSearchTokenKeysWithWeights.searchBySymbol(tokenKeys[0].symbol);
expect(result).toEqual(tokenKeys[0]);
});
it('should return weight tokenKey when found by weight symbol', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
const match = tokenKeys.find(tk => tk.key === weightTokenKey.key);
const result = gl.FilteredSearchTokenKeysWithWeights.searchBySymbol(weightTokenKey.symbol);
expect(result).toEqual(match);
});
});
describe('searchByKeyParam', () => {
it('should return null when key param not found', () => {
const tokenKey = gl.FilteredSearchTokenKeysWithWeights.searchByKeyParam('notakeyparam');
expect(tokenKey === null).toBe(true);
});
it('should return tokenKey when found by key param', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
const result = gl.FilteredSearchTokenKeysWithWeights
.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
expect(result).toEqual(tokenKeys[0]);
});
it('should return alternative tokenKey when found by key param', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.getAlternatives();
const result = gl.FilteredSearchTokenKeysWithWeights
.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
expect(result).toEqual(tokenKeys[0]);
});
it('should return weight tokenKey when found by weight key param', () => {
const tokenKeys = gl.FilteredSearchTokenKeysWithWeights.get();
const match = tokenKeys.find(tk => tk.key === weightTokenKey.key);
const result = gl.FilteredSearchTokenKeysWithWeights.searchByKeyParam(weightTokenKey.key);
expect(result).toEqual(match);
});
});
describe('searchByConditionUrl', () => {
it('should return null when condition url not found', () => {
const condition = gl.FilteredSearchTokenKeysWithWeights.searchByConditionUrl(null);
expect(condition === null).toBe(true);
});
it('should return condition when found by url', () => {
const conditions = gl.FilteredSearchTokenKeysWithWeights.getConditions();
const result = gl.FilteredSearchTokenKeysWithWeights
.searchByConditionUrl(conditions[0].url);
expect(result).toBe(conditions[0]);
});
it('should return weight condition when found by weight url', () => {
const conditions = gl.FilteredSearchTokenKeysWithWeights.getConditions();
const weightConditions = conditions.filter(c => c.tokenKey === 'weight');
const result = gl.FilteredSearchTokenKeysWithWeights
.searchByConditionUrl(weightConditions[0].url);
expect(result).toBe(weightConditions[0]);
});
});
describe('searchByConditionKeyValue', () => {
it('should return null when condition tokenKey and value not found', () => {
const condition = gl.FilteredSearchTokenKeysWithWeights
.searchByConditionKeyValue(null, null);
expect(condition === null).toBe(true);
});
it('should return condition when found by tokenKey and value', () => {
const conditions = gl.FilteredSearchTokenKeysWithWeights.getConditions();
const result = gl.FilteredSearchTokenKeysWithWeights
.searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
expect(result).toEqual(conditions[0]);
});
it('should return weight condition when found by weight tokenKey and value', () => {
const conditions = gl.FilteredSearchTokenKeysWithWeights.getConditions();
const weightConditions = conditions.filter(c => c.tokenKey === 'weight');
const result = gl.FilteredSearchTokenKeysWithWeights
.searchByConditionKeyValue(weightConditions[0].tokenKey, weightConditions[0].value);
expect(result).toEqual(weightConditions[0]);
});
});
});
})();
...@@ -89,8 +89,8 @@ require('vendor/fuzzaldrin-plus'); ...@@ -89,8 +89,8 @@ require('vendor/fuzzaldrin-plus');
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName;
mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName;
a1 = "a[href='" + issuesAssignedToMeLink + "']"; a1 = "a[href='" + issuesAssignedToMeLink + "']";
a2 = "a[href='" + issuesIHaveCreatedLink + "']"; a2 = "a[href='" + issuesIHaveCreatedLink + "']";
a3 = "a[href='" + mrsAssignedToMeLink + "']"; a3 = "a[href='" + mrsAssignedToMeLink + "']";
......
...@@ -1266,55 +1266,55 @@ describe API::Issues, api: true do ...@@ -1266,55 +1266,55 @@ describe API::Issues, api: true do
end end
end end
describe 'POST :id/issues/:issue_id/subscription' do describe 'POST :id/issues/:issue_id/subscribe' do
it 'subscribes to an issue' do it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true) expect(json_response['subscribed']).to eq(true)
end end
it 'returns 304 if already subscribed' do it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
it 'returns 404 if the issue is not found' do it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/subscription", user) post api("/projects/#{project.id}/issues/123/subscribe", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
it 'returns 404 if the issue is confidential' do it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
describe 'DELETE :id/issues/:issue_id/subscription' do describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do it 'unsubscribes from an issue' do
delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false) expect(json_response['subscribed']).to eq(false)
end end
it 'returns 304 if not subscribed' do it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2)
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
it 'returns 404 if the issue is not found' do it 'returns 404 if the issue is not found' do
delete api("/projects/#{project.id}/issues/123/subscription", user) post api("/projects/#{project.id}/issues/123/unsubscribe", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
it 'returns 404 if the issue is confidential' do it 'returns 404 if the issue is confidential' do
delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
......
...@@ -318,10 +318,10 @@ describe API::Labels, api: true do ...@@ -318,10 +318,10 @@ describe API::Labels, api: true do
end end
end end
describe "POST /projects/:id/labels/:label_id/subscription" do describe "POST /projects/:id/labels/:label_id/subscribe" do
context "when label_id is a label title" do context "when label_id is a label title" do
it "subscribes to the label" do it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) post api("/projects/#{project.id}/labels/#{label1.title}/subscribe", user)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title) expect(json_response["name"]).to eq(label1.title)
...@@ -331,7 +331,7 @@ describe API::Labels, api: true do ...@@ -331,7 +331,7 @@ describe API::Labels, api: true do
context "when label_id is a label ID" do context "when label_id is a label ID" do
it "subscribes to the label" do it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title) expect(json_response["name"]).to eq(label1.title)
...@@ -343,7 +343,7 @@ describe API::Labels, api: true do ...@@ -343,7 +343,7 @@ describe API::Labels, api: true do
before { label1.subscribe(user, project) } before { label1.subscribe(user, project) }
it "returns 304" do it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
...@@ -351,21 +351,21 @@ describe API::Labels, api: true do ...@@ -351,21 +351,21 @@ describe API::Labels, api: true do
context "when label ID is not found" do context "when label ID is not found" do
it "returns 404 error" do it "returns 404 error" do
post api("/projects/#{project.id}/labels/1234/subscription", user) post api("/projects/#{project.id}/labels/1234/subscribe", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
end end
describe "DELETE /projects/:id/labels/:label_id/subscription" do describe "POST /projects/:id/labels/:label_id/unsubscribe" do
before { label1.subscribe(user, project) } before { label1.subscribe(user, project) }
context "when label_id is a label title" do context "when label_id is a label title" do
it "unsubscribes from the label" do it "unsubscribes from the label" do
delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) post api("/projects/#{project.id}/labels/#{label1.title}/unsubscribe", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title) expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey expect(json_response["subscribed"]).to be_falsey
end end
...@@ -373,9 +373,9 @@ describe API::Labels, api: true do ...@@ -373,9 +373,9 @@ describe API::Labels, api: true do
context "when label_id is a label ID" do context "when label_id is a label ID" do
it "unsubscribes from the label" do it "unsubscribes from the label" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title) expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey expect(json_response["subscribed"]).to be_falsey
end end
...@@ -385,7 +385,7 @@ describe API::Labels, api: true do ...@@ -385,7 +385,7 @@ describe API::Labels, api: true do
before { label1.unsubscribe(user, project) } before { label1.unsubscribe(user, project) }
it "returns 304" do it "returns 304" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
...@@ -393,7 +393,7 @@ describe API::Labels, api: true do ...@@ -393,7 +393,7 @@ describe API::Labels, api: true do
context "when label ID is not found" do context "when label ID is not found" do
it "returns 404 error" do it "returns 404 error" do
delete api("/projects/#{project.id}/labels/1234/subscription", user) post api("/projects/#{project.id}/labels/1234/unsubscribe", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
......
...@@ -740,22 +740,22 @@ describe API::MergeRequests, api: true do ...@@ -740,22 +740,22 @@ describe API::MergeRequests, api: true do
end end
end end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do describe 'POST :id/merge_requests/:merge_request_id/subscribe' do
it 'subscribes to a merge request' do it 'subscribes to a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true) expect(json_response['subscribed']).to eq(true)
end end
it 'returns 304 if already subscribed' do it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
it 'returns 404 if the merge request is not found' do it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user) post api("/projects/#{project.id}/merge_requests/123/subscribe", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
...@@ -764,28 +764,28 @@ describe API::MergeRequests, api: true do ...@@ -764,28 +764,28 @@ describe API::MergeRequests, api: true do
guest = create(:user) guest = create(:user)
project.team << [guest, :guest] project.team << [guest, :guest]
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest)
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
end end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do
it 'unsubscribes from a merge request' do it 'unsubscribes from a merge request' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false) expect(json_response['subscribed']).to eq(false)
end end
it 'returns 304 if not subscribed' do it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin)
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
it 'returns 404 if the merge request is not found' do it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user) post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
...@@ -794,7 +794,7 @@ describe API::MergeRequests, api: true do ...@@ -794,7 +794,7 @@ describe API::MergeRequests, api: true do
guest = create(:user) guest = create(:user)
project.team << [guest, :guest] project.team << [guest, :guest]
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest)
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
......
...@@ -67,4 +67,86 @@ describe API::V3::Labels, api: true do ...@@ -67,4 +67,86 @@ describe API::V3::Labels, api: true do
expect(priority_label_response['subscribed']).to be_falsey expect(priority_label_response['subscribed']).to be_falsey
end end
end end
describe "POST /projects/:id/labels/:label_id/subscription" do
context "when label_id is a label title" do
it "subscribes to the label" do
post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_truthy
end
end
context "when label_id is a label ID" do
it "subscribes to the label" do
post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_truthy
end
end
context "when user is already subscribed to label" do
before { label1.subscribe(user, project) }
it "returns 304" do
post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(304)
end
end
context "when label ID is not found" do
it "returns 404 error" do
post v3_api("/projects/#{project.id}/labels/1234/subscription", user)
expect(response).to have_http_status(404)
end
end
end
describe "DELETE /projects/:id/labels/:label_id/subscription" do
before { label1.subscribe(user, project) }
context "when label_id is a label title" do
it "unsubscribes from the label" do
delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
expect(response).to have_http_status(200)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
end
context "when label_id is a label ID" do
it "unsubscribes from the label" do
delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(200)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
end
context "when user is already unsubscribed from label" do
before { label1.unsubscribe(user, project) }
it "returns 304" do
delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(304)
end
end
context "when label ID is not found" do
it "returns 404 error" do
delete v3_api("/projects/#{project.id}/labels/1234/subscription", user)
expect(response).to have_http_status(404)
end
end
end
end end
module FilteredSearchHelpers
def filtered_search
page.find('.filtered-search')
end
def input_filtered_search(search_term, submit: true)
filtered_search.set(search_term)
if submit
filtered_search.send_keys(:enter)
end
end
def input_filtered_search_keys(search_term)
filtered_search.send_keys(search_term)
filtered_search.send_keys(:enter)
end
def expect_filtered_search_input(input)
expect(find('.filtered-search').value).to eq(input)
end
def clear_search_field
find('.filtered-search-input-container .clear-search').click
end
def reset_filters
clear_search_field
filtered_search.send_keys(:enter)
end
def init_label_search
filtered_search.set('label:')
# This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end
end
...@@ -10,4 +10,13 @@ module MergeRequestHelpers ...@@ -10,4 +10,13 @@ module MergeRequestHelpers
def last_merge_request def last_merge_request
page.all('ul.mr-list > li').last.text page.all('ul.mr-list > li').last.text
end end
def expect_mr_list_count(open_count, closed_count = 0)
all_count = open_count + closed_count
expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
page.within '.mr-list' do
expect(page).to have_selector('.merge-request', count: open_count)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment