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:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
ELASTIC_HOST: "registry.gitlab.com-gitlab-org-test-elastic-image"
ELASTIC_HOST: "elasticsearch"
RAILS_ENV: "test"
SIMPLECOV: "true"
SETUP_DB: "true"
......@@ -17,6 +17,8 @@ variables:
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
# This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms600m -Xmx600m"
before_script:
- source ./scripts/prepare_build.sh
......@@ -55,7 +57,7 @@ stages:
services:
- mysql:latest
- redis:alpine
- registry.gitlab.com/gitlab-org/test-elastic-image
- elasticsearch:5.1
.rspec-knapsack: &rspec-knapsack
stage: test
......
......@@ -104,9 +104,10 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Search
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git"
gem 'elasticsearch-model', '~> 0.1.9'
gem 'elasticsearch-rails', '~> 0.1.9'
gem 'elasticsearch-api', '5.0.3'
gem 'gitlab-elasticsearch-git', '1.1.1', require: "elasticsearch/git"
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
......
......@@ -86,7 +86,7 @@ GEM
sass (>= 3.3.4)
brakeman (3.4.1)
browser (2.2.0)
builder (3.2.2)
builder (3.2.3)
bullet (5.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
......@@ -169,17 +169,17 @@ GEM
railties (>= 4.2)
dropzonejs-rails (0.7.2)
rails (> 3.1)
elasticsearch (1.0.15)
elasticsearch-api (= 1.0.15)
elasticsearch-transport (= 1.0.15)
elasticsearch-api (1.0.15)
elasticsearch (5.0.3)
elasticsearch-api (= 5.0.3)
elasticsearch-transport (= 5.0.3)
elasticsearch-api (5.0.3)
multi_json
elasticsearch-model (0.1.8)
elasticsearch-model (0.1.9)
activesupport (> 3)
elasticsearch (> 0.4)
hashie
elasticsearch-rails (0.1.8)
elasticsearch-transport (1.0.15)
elasticsearch-rails (0.1.9)
elasticsearch-transport (5.0.3)
faraday
multi_json
email_reply_trimmer (0.1.6)
......@@ -265,12 +265,12 @@ GEM
mime-types (>= 1.19)
rugged (>= 0.23.0b)
github-markup (1.4.0)
gitlab-elasticsearch-git (1.0.1)
gitlab-elasticsearch-git (1.1.1)
activemodel (~> 4.2)
activesupport (~> 4.2)
charlock_holmes (~> 0.7)
elasticsearch-api (~> 1.0)
elasticsearch-model (~> 0.1.8)
elasticsearch-api
elasticsearch-model (~> 0.1.9)
github-linguist (~> 4.7)
rugged (~> 0.24)
gitlab-flowdock-git-hook (1.0.1)
......@@ -353,7 +353,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
hashie (3.4.4)
hashie (3.5.1)
health_check (2.2.1)
rails (>= 4.0)
hipchat (1.5.2)
......@@ -889,8 +889,9 @@ DEPENDENCIES
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1)
elasticsearch-model
elasticsearch-rails
elasticsearch-api (= 5.0.3)
elasticsearch-model (~> 0.1.9)
elasticsearch-rails (~> 0.1.9)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
......@@ -908,7 +909,7 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.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-license (~> 1.0)
gitlab-markup (~> 1.5.1)
......
......@@ -76,7 +76,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager();
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
}
Issuable.init();
new gl.IssuableBulkActions({
......
......@@ -37,27 +37,18 @@ require('./filtered_search_dropdown');
}
renderContent() {
const dropdownData = [{
icon: 'fa-pencil',
hint: 'author:',
tag: '<@author>',
}, {
icon: 'fa-user',
hint: 'assignee:',
tag: '<@assignee>',
}, {
icon: 'fa-clock-o',
hint: 'milestone:',
tag: '<%milestone>',
}, {
icon: 'fa-tag',
hint: 'label:',
tag: '<~label>',
}, {
icon: 'fa-balance-scale',
hint: 'weight:',
tag: '<weight>',
}];
const dropdownData = [];
[].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag } = dropdownMenu.dataset;
if (icon && hint && tag) {
dropdownData.push({
icon: `fa-${icon}`,
hint,
tag: `<${tag}>`,
});
}
});
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
this.droplab.setData(this.hookId, dropdownData);
......
......@@ -52,8 +52,9 @@
}
renderContent(forceShowList = false) {
if (forceShowList && this.getCurrentHook().list.hidden) {
this.getCurrentHook().list.show();
const currentHook = this.getCurrentHook();
if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show();
}
}
......@@ -92,18 +93,23 @@
}
hideDropdown() {
this.getCurrentHook().list.hide();
const currentHook = this.getCurrentHook();
if (currentHook) {
currentHook.list.hide();
}
}
resetFilters() {
const hook = this.getCurrentHook();
const data = hook.list.data;
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
return updated;
});
hook.list.render(results);
if (hook) {
const data = hook.list.data;
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
return updated;
});
hook.list.render(results);
}
}
}
......
......@@ -2,10 +2,16 @@
(() => {
class FilteredSearchDropdownManager {
constructor(baseEndpoint = '') {
constructor(baseEndpoint = '', page) {
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchInput = document.querySelector('.filtered-search');
this.page = page;
if (this.page === 'issues') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights;
}
this.setupMapping();
......@@ -48,17 +54,20 @@
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: document.querySelector('#js-dropdown-label'),
},
weight: {
reference: null,
gl: 'DropdownNonUser',
element: document.querySelector('#js-dropdown-weight'),
},
hint: {
reference: null,
gl: 'DropdownHint',
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 = '') {
......@@ -155,7 +164,7 @@
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
&& this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
......
(() => {
class FilteredSearchManager {
constructor() {
constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (page === 'issues') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights;
}
if (this.filteredSearchInput) {
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.loadSearchParamsFromURL();
......@@ -118,7 +123,7 @@
const value = split[1];
// Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p);
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`);
......@@ -126,7 +131,7 @@
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam);
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
const indexOf = keyParam.indexOf('_');
......@@ -171,9 +176,9 @@
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
const condition = gl.FilteredSearchTokenKeys
const condition = this.filteredSearchTokenKeys
.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;
let tokenPath = '';
......
......@@ -19,11 +19,6 @@
type: 'array',
param: 'name[]',
symbol: '~',
}, {
key: 'weight',
type: 'string',
param: '',
symbol: '',
}];
const alternativeTokenKeys = [{
......@@ -51,14 +46,6 @@
url: 'label_name[]=No+Label',
tokenKey: 'label',
value: 'none',
}, {
url: 'weight=No+Weight',
tokenKey: 'weight',
value: 'none',
}, {
url: 'weight=Any+Weight',
tokenKey: 'weight',
value: 'any',
}];
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 @@
url: issuesPath + "/?author_username=" + userName
}, 'separator', {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId
url: mrPath + "/?assignee_username=" + userName
}, {
text: "Merge requests I've created",
url: mrPath + "/?author_id=" + userId
url: mrPath + "/?author_username=" + userName
}
];
if (!name) {
......
......@@ -53,6 +53,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, labels_params).execute
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|
format.html
format.json do
......
......@@ -7,14 +7,14 @@ module Elastic
mappings _parent: { type: 'project' } do
indexes :id, type: :integer
indexes :iid, type: :integer, index: :not_analyzed
indexes :title, type: :string,
indexes :iid, type: :integer
indexes :title, type: :text,
index_options: 'offsets'
indexes :description, type: :string,
indexes :description, type: :text,
index_options: 'offsets'
indexes :created_at, type: :date
indexes :updated_at, type: :date
indexes :state, type: :string
indexes :state, type: :text
indexes :project_id, type: :integer
indexes :author_id, type: :integer
indexes :assignee_id, type: :integer
......
......@@ -8,18 +8,18 @@ module Elastic
mappings _parent: { type: 'project' } do
indexes :id, type: :integer
indexes :iid, type: :integer
indexes :target_branch, type: :string,
indexes :target_branch, type: :text,
index_options: 'offsets'
indexes :source_branch, type: :string,
indexes :source_branch, type: :text,
index_options: 'offsets'
indexes :title, type: :string,
indexes :title, type: :text,
index_options: 'offsets'
indexes :description, type: :string,
indexes :description, type: :text,
index_options: 'offsets'
indexes :created_at, type: :date
indexes :updated_at, type: :date
indexes :state, type: :string
indexes :merge_status, type: :string
indexes :state, type: :text
indexes :merge_status, type: :text
indexes :source_project_id, type: :integer
indexes :target_project_id, type: :integer
indexes :author_id, type: :integer
......
......@@ -7,9 +7,9 @@ module Elastic
mappings _parent: { type: 'project' } do
indexes :id, type: :integer
indexes :title, type: :string,
indexes :title, type: :text,
index_options: 'offsets'
indexes :description, type: :string,
indexes :description, type: :text,
index_options: 'offsets'
indexes :project_id, type: :integer
indexes :created_at, type: :date
......
......@@ -7,7 +7,7 @@ module Elastic
mappings _parent: { type: 'project' } do
indexes :id, type: :integer
indexes :note, type: :string,
indexes :note, type: :text,
index_options: 'offsets'
indexes :project_id, type: :integer
indexes :created_at, type: :date
......
......@@ -7,16 +7,16 @@ module Elastic
mappings do
indexes :id, type: :integer
indexes :name, type: :string,
indexes :name, type: :text,
index_options: 'offsets'
indexes :path, type: :string,
indexes :path, type: :text,
index_options: 'offsets'
indexes :name_with_namespace, type: :string,
indexes :name_with_namespace, type: :text,
index_options: 'offsets',
analyzer: :my_ngram_analyzer
indexes :path_with_namespace, type: :string,
indexes :path_with_namespace, type: :text,
index_options: 'offsets'
indexes :description, type: :string,
indexes :description, type: :text,
index_options: 'offsets'
indexes :namespace_id, type: :integer
indexes :created_at, type: :date
......@@ -91,7 +91,7 @@ module Elastic
}
end
query_hash[:query][:bool][:filter] = { and: filters }
query_hash[:query][:bool][:filter] = filters
query_hash[:sort] = [:_score]
......
......@@ -7,15 +7,15 @@ module Elastic
mappings do
indexes :id, type: :integer
indexes :title, type: :string,
indexes :title, type: :text,
index_options: 'offsets'
indexes :file_name, type: :string,
indexes :file_name, type: :text,
index_options: 'offsets'
indexes :content, type: :string,
indexes :content, type: :text,
index_options: 'offsets'
indexes :created_at, type: :date
indexes :updated_at, type: :date
indexes :state, type: :string
indexes :state, type: :text
indexes :project_id, type: :integer
indexes :author_id, type: :integer
indexes :visibility_level, type: :integer
......
......@@ -5,18 +5,19 @@
= render "projects/issues/head"
= render 'projects/last_push'
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search')
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.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))
- 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
New Merge Request
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
......@@ -32,7 +32,7 @@
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{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 }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
......@@ -42,7 +42,7 @@
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-assignee.dropdown-menu
#js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
......@@ -57,7 +57,7 @@
{{name}}
%span.dropdown-light-content
@{{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 }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
......@@ -70,7 +70,7 @@
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
{{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 }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
......@@ -82,19 +82,20 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
#js-dropdown-weight.dropdown-menu{ 'data-dropdown' => true }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
No Weight
%li.filter-dropdown-item{ 'data-value' => 'any' }
%button.btn.btn-link
Any Weight
%li.divider
%ul.filter-dropdown{ 'data-dropdown' => true }
- Issue.weight_filter_options.each do |weight|
%li.filter-dropdown-item{ 'data-value' => "#{weight}" }
%button.btn.btn-link= weight
- if type == :issues
#js-dropdown-weight.dropdown-menu{ data: { icon: 'balance-scale', hint: 'weight', tag: 'weight' } }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
No Weight
%li.filter-dropdown-item{ 'data-value' => 'any' }
%button.btn.btn-link
Any Weight
%li.divider
%ul.filter-dropdown{ 'data-dropdown' => true }
- Issue.weight_filter_options.each do |weight|
%li.filter-dropdown-item{ 'data-value' => "#{weight}" }
%button.btn.btn-link= weight
.pull-right
= render 'shared/sort_dropdown', type: local_assigns[:type]
......
......@@ -36,7 +36,7 @@ class ElasticIndexerWorker
client.delete index: klass.index_name, type: klass.document_type, id: record_id
end
clear_project_indexes(record_id) if klass == Project
clear_project_data(record_id) if klass == Project
end
rescue Elasticsearch::Transport::Transport::Errors::NotFound, ActiveRecord::RecordNotFound
# These errors can happen in several cases, including:
......@@ -56,43 +56,33 @@ class ElasticIndexerWorker
end
end
def clear_project_indexes(record_id)
remove_repository_index(record_id)
remove_wiki_index(record_id)
remove_nested_content(record_id)
def clear_project_data(record_id)
remove_children_documents(Repository.document_type, record_id)
remove_children_documents(ProjectWiki.document_type, record_id)
remove_children_documents(MergeRequest.document_type, record_id)
remove_documents_by_project_id(record_id)
end
def remove_repository_index(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)
def remove_documents_by_project_id(record_id)
client.delete_by_query({
index: Project.__elasticsearch__.index_name,
body: {
query: {
term: { "_parent" => record_id }
term: { "project_id" => record_id }
}
}
})
end
def remove_wiki_index(record_id)
def remove_children_documents(document_type, parent_record_id)
client.delete_by_query({
index: ProjectWiki.__elasticsearch__.index_name,
index: Project.__elasticsearch__.index_name,
body: {
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`
is returned.
```
POST /projects/:id/issues/:issue_id/subscription
POST /projects/:id/issues/:issue_id/subscribe
```
| Attribute | Type | Required | Description |
......@@ -532,7 +532,7 @@ POST /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue |
```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:
......@@ -579,7 +579,7 @@ from it. If the user is not subscribed to the issue, the
status code `304` is returned.
```
DELETE /projects/:id/issues/:issue_id/subscription
POST /projects/:id/issues/:issue_id/unsubscribe
```
| Attribute | Type | Required | Description |
......@@ -588,7 +588,7 @@ DELETE /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue |
```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:
......
......@@ -188,12 +188,12 @@ Example response:
## 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`
is returned.
```
POST /projects/:id/labels/:label_id/subscription
POST /projects/:id/labels/:label_id/subscribe
```
| Attribute | Type | Required | Description |
......@@ -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 |
```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:
......@@ -228,7 +228,7 @@ from it. If the user is not subscribed to the label, the
status code `304` is returned.
```
DELETE /projects/:id/labels/:label_id/subscription
POST /projects/:id/labels/:label_id/unsubscribe
```
| Attribute | Type | Required | Description |
......@@ -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 |
```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:
......
......@@ -810,7 +810,7 @@ Subscribes the authenticated user to a merge request to receive notification. If
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 |
......@@ -819,7 +819,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request |
```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:
......@@ -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.
```
DELETE /projects/:id/merge_requests/:merge_request_id/subscription
POST /projects/:id/merge_requests/:merge_request_id/subscribe
```
| Attribute | Type | Required | Description |
......@@ -893,7 +893,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request |
```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:
......
......@@ -28,10 +28,10 @@ changes are in V4:
- 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
- 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)
- Renamed param `branch_name` to `branch` on the following endpoints
- POST `:id/repository/branches`
- POST `:id/repository/commits`
- POST/PUT/DELETE `:id/repository/files`
- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response
......@@ -29,12 +29,7 @@ GitLab, or on a separate server.
## Requirements
These are the requirements needed for Elasticsearch to work:
- 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.
Elasticsearch 5.1.x.
## Install Elasticsearch
......
......@@ -299,13 +299,6 @@ Feature: Project Merge Requests
And I preview a description text like "Bug fixed :smile:"
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
Scenario: I can unsubscribe from merge request
Given I visit merge request page "Bug NS-04"
......
......@@ -19,6 +19,7 @@ module API
mount ::API::V3::Projects
mount ::API::V3::ProjectSnippets
mount ::API::V3::Repositories
mount ::API::V3::Subscriptions
mount ::API::V3::SystemHooks
mount ::API::V3::Tags
mount ::API::V3::Templates
......
......@@ -21,7 +21,7 @@ module API
desc 'Subscribe to a resource' do
success entity_class
end
post ":id/#{type}/:subscribable_id/subscription" do
post ":id/#{type}/:subscribable_id/subscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project)
......@@ -35,7 +35,7 @@ module API
desc 'Unsubscribe from a resource' do
success entity_class
end
delete ":id/#{type}/:subscribable_id/subscription" do
post ":id/#{type}/:subscribable_id/unsubscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
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
client = Elasticsearch::Client.new(host: ApplicationSetting.current.elasticsearch_host,
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"])
if version.major == 2 && version.minor == 4
if version.major == 5 && version.minor == 1
puts "yes (#{version})".color(:green)
else
puts "no, you have #{version}".color(:red)
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
def check_gitlab_geo_node(node)
......
require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do
include FilteredSearchHelpers
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
......@@ -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') }
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)
init_label_search
filtered_search.send_keys(label)
......
require 'rails_helper'
describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax
let!(:group) { create(:group) }
......@@ -17,19 +18,6 @@ describe 'Filter issues', js: true, feature: true do
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(: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
page.within '.issues-list' do
......
require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax
let(:project) { create(:project, :public) }
......@@ -32,123 +34,77 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context 'filter by label bug' do
before do
select_labels('bug')
input_filtered_search('label:~bug')
end
it 'apply the filter' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
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
context 'filter by label feature' do
before do
select_labels('feature')
input_filtered_search('label:~feature')
end
it 'applies the filter' do
expect(page).to have_content "Feature1"
expect(page).not_to have_content "Bugfix2"
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
context 'filter by label enhancement' do
before do
select_labels('enhancement')
input_filtered_search('label:~enhancement')
end
it 'applies the filter' do
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
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
context 'filter by label enhancement and bug in issues list' do
before do
select_labels('bug', 'enhancement')
input_filtered_search('label:~bug label:~enhancement')
end
it 'applies the filters' do
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content "Bugfix2"
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
context 'remove filtered labels' do
context 'clear button' do
before do
page.within '.labels-filter' do
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
input_filtered_search('label:~bug')
end
it 'allows user to remove filtered labels' do
first('.js-label-filter-remove').click
wait_for_ajax
first('.clear-search').click
filtered_search.send_keys(:enter)
expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
expect(find('.labels-filter')).not_to have_content 'bug'
expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
expect(page).to have_content "Bugfix2"
expect(page).to have_content "Feature1"
expect(page).to have_content "Bugfix1"
end
end
context 'dropdown filtering' do
context 'filter dropdown' do
it 'filters by label name' do
page.within '.labels-filter' do
click_button 'Label'
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
init_label_search
filtered_search.send_keys('~bug')
def select_labels(*labels)
page.find('.js-label-select').click
wait_for_ajax
labels.each do |label|
execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
page.within '.filter-dropdown' do
expect(page).not_to have_content 'enhancement'
expect(page).to have_content 'bug'
end
end
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end
end
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
include FilteredSearchHelpers
include MergeRequestHelpers
let(:project) { create(:project, :public) }
let!(:user) { create(:user)}
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
project.team << [user, :master]
login_as(user)
......@@ -15,42 +23,42 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project, milestone: milestone)
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_css('.merge-request', count: 1)
end
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, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0)
end
it 'shows issues in future' do
it 'shows merge requests in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
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_css('.merge-request', count: 1)
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)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0)
end
......@@ -61,7 +69,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: 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_css('.merge-request', count: 1)
......@@ -77,19 +85,10 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: 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_css('.merge-request', count: 1)
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
require 'rails_helper'
feature 'Issues filter reset button', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax
include IssueHelpers
let!(:project) { create(:project, :public) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:bug) { create(:label, project: project, name: 'bug')}
let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
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!(: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
mr2.labels << bug
......@@ -50,7 +53,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when author filter has been applied' 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)
reset_filters
......@@ -60,7 +63,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when assignee filter has been applied' 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)
reset_filters
......@@ -70,7 +73,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when all filters have been applied' 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)
reset_filters
......@@ -82,15 +85,7 @@ feature 'Issues filter reset button', feature: true, js: true do
it 'the reset link should not be visible' do
visit_merge_requests(project)
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
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
......@@ -186,7 +186,7 @@ describe "Search", feature: true do
sleep 2
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
it 'takes user to her MR page when MR authored is clicked' do
......@@ -194,7 +194,7 @@ describe "Search", feature: true do
sleep 2
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
......
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');
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName;
mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName;
a1 = "a[href='" + issuesAssignedToMeLink + "']";
a2 = "a[href='" + issuesIHaveCreatedLink + "']";
a3 = "a[href='" + mrsAssignedToMeLink + "']";
......
......@@ -1266,55 +1266,55 @@ describe API::Issues, api: true do
end
end
describe 'POST :id/issues/:issue_id/subscription' do
describe 'POST :id/issues/:issue_id/subscribe' 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(json_response['subscribed']).to eq(true)
end
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)
end
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)
end
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)
end
end
describe 'DELETE :id/issues/:issue_id/subscription' do
describe 'POST :id/issues/:issue_id/unsubscribe' 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)
end
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)
end
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)
end
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)
end
......
......@@ -318,10 +318,10 @@ describe API::Labels, api: true do
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
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(json_response["name"]).to eq(label1.title)
......@@ -331,7 +331,7 @@ describe API::Labels, api: true do
context "when label_id is a label ID" 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(json_response["name"]).to eq(label1.title)
......@@ -343,7 +343,7 @@ describe API::Labels, api: true do
before { label1.subscribe(user, project) }
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)
end
......@@ -351,21 +351,21 @@ describe API::Labels, api: true do
context "when label ID is not found" 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)
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) }
context "when label_id is a label title" 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["subscribed"]).to be_falsey
end
......@@ -373,9 +373,9 @@ describe API::Labels, api: true do
context "when label_id is a label ID" 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["subscribed"]).to be_falsey
end
......@@ -385,7 +385,7 @@ describe API::Labels, api: true do
before { label1.unsubscribe(user, project) }
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)
end
......@@ -393,7 +393,7 @@ describe API::Labels, api: true do
context "when label ID is not found" 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)
end
......
......@@ -740,22 +740,22 @@ describe API::MergeRequests, api: true do
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
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(json_response['subscribed']).to eq(true)
end
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)
end
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)
end
......@@ -764,28 +764,28 @@ describe API::MergeRequests, api: true do
guest = create(:user)
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)
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
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)
end
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)
end
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)
end
......@@ -794,7 +794,7 @@ describe API::MergeRequests, api: true do
guest = create(:user)
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)
end
......
......@@ -67,4 +67,86 @@ describe API::V3::Labels, api: true do
expect(priority_label_response['subscribed']).to be_falsey
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
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
def last_merge_request
page.all('ul.mr-list > li').last.text
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
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