Commit 90e5de3c authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee' into 'master'

Ce to ee

Closes gitlab-ce#25146

See merge request !1054
parents bcdf6deb a73a99af
......@@ -109,18 +109,19 @@ gem 'elasticsearch-rails'
gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git"
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.6'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......
......@@ -54,6 +54,8 @@ GEM
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.6)
asciidoctor (~> 1.5)
ast (2.3.0)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
......@@ -868,6 +870,7 @@ DEPENDENCIES
allocations (~> 1.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.6)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
......
......@@ -60,6 +60,7 @@
/*= require_directory ./extensions */
/*= require_directory ./lib/utils */
/*= require_directory ./u2f */
/*= require_directory ./droplab */
/*= require_directory . */
/*= require fuzzaldrin-plus */
/*= require es6-promise.auto */
......
......@@ -5,6 +5,7 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
var AUTO_SCROLL_OFFSET = 75;
var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() {
Build.interval = null;
......@@ -26,7 +27,7 @@
this.$autoScrollStatus = $('#autoscroll-status');
this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
this.$upBuildTrace = $('#up-build-trace');
this.$downBuildTrace = $('#down-build-trace');
this.$downBuildTrace = $(DOWN_BUILD_TRACE);
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
......@@ -91,6 +92,9 @@
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
}
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
......@@ -105,6 +109,8 @@
dataType: "json",
success: (function(_this) {
return function(log) {
var pageUrl;
if (log.state) {
_this.state = log.state;
}
......@@ -116,7 +122,12 @@
}
return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) {
return Turbolinks.visit(_this.pageUrl);
pageUrl = _this.pageUrl;
if (_this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
return Turbolinks.visit(pageUrl);
}
};
})(this)
......
......@@ -86,6 +86,9 @@
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
......
This diff is collapsed.
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
function droplabAjaxException(message) {
this.message = message;
}
w.droplabAjax = {
_loadUrlData: function _loadUrlData(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
if (!config || !config.endpoint || !config.method) {
return;
}
if (config.method !== 'setData' && config.method !== 'addData') {
return;
}
if (config.loadingTemplate) {
var dynamicList = hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', '');
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
hook.list[config.method].call(hook.list, d);
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
\ No newline at end of file
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
w.droplabAjaxFilter = {
init: function(hook) {
this.destroyed = false;
this.hook = hook;
this.notLoading();
this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
this.trigger(true);
},
notLoading: function notLoading() {
this.loading = false;
},
debounceTrigger: function debounceTrigger(e) {
var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
var focusEvent = e.type === 'focus';
if (invalidKeyPressed || this.loading) {
return;
}
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
},
trigger: function trigger(getEntireList) {
var config = this.hook.config.droplabAjaxFilter;
var searchValue = this.trigger.value;
if (!config || !config.endpoint || !config.searchKey) {
return;
}
if (config.searchValueFunction) {
searchValue = config.searchValueFunction();
}
if (config.loadingTemplate && this.hook.list.data === undefined ||
this.hook.list.data.length === 0) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', true);
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (getEntireList) {
searchValue = '';
}
if (config.searchKey === searchValue) {
return this.list.show();
}
this.loading = true;
var params = config.params || {};
params[config.searchKey] = searchValue;
var self = this;
this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) {
if (config.loadingTemplate && self.hook.list.data === undefined ||
self.hook.list.data.length === 0) {
const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
if (!self.destroyed) {
var hookListChildren = self.hook.list.list.children;
var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
if (onlyDynamicList && data.length === 0) {
self.hook.list.hide();
}
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
});
},
_loadUrlData: function _loadUrlData(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
buildParams: function(params) {
if (!params) return '';
var paramsArray = Object.keys(params).map(function(param) {
return param + '=' + (params[param] || '');
});
return '?' + paramsArray.join('&');
},
destroy: function destroy() {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.destroyed = true;
this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
\ No newline at end of file
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
var config = e.detail.hook.config.droplabFilter;
var matches = [];
var filterFunction;
// will only work on dynamically set data
if(!data){
return;
}
if (config && config.filterFunction && typeof config.filterFunction === 'function') {
filterFunction = config.filterFunction;
} else {
filterFunction = function(o){
// cheap string search
o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
return o;
};
}
matches = data.map(function(o) {
return filterFunction(o, value);
});
list.render(matches);
},
init: function init(hookInput) {
var config = hookInput.config.droplabFilter;
if (!config || (!config.template && !config.filterFunction)) {
return;
}
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
\ No newline at end of file
/*= require filtered_search/filtered_search_dropdown */
/* global droplabFilter */
(() => {
class DropdownHint extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter);
this.config = {
droplabFilter: {
template: 'hint',
filterFunction: gl.DropdownUtils.filterHint,
},
};
}
itemClicked(e) {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
this.dismissDropdown();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) {
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
}
this.dismissDropdown();
this.dispatchInputEvent();
}
}
}
renderContent() {
const dropdownData = [{
icon: 'fa-pencil',
hint: 'author:',
tag: '&lt;@author&gt;',
}, {
icon: 'fa-user',
hint: 'assignee:',
tag: '&lt;@assignee&gt;',
}, {
icon: 'fa-clock-o',
hint: 'milestone:',
tag: '&lt;%milestone&gt;',
}, {
icon: 'fa-tag',
hint: 'label:',
tag: '&lt;~label&gt;',
}];
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
this.droplab.setData(this.hookId, dropdownData);
}
init() {
this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownHint = DropdownHint;
})();
/*= require filtered_search/filtered_search_dropdown */
/* global droplabAjax */
/* global droplabFilter */
(() => {
class DropdownNonUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter, endpoint, symbol) {
super(droplab, dropdown, input, filter);
this.symbol = symbol;
this.config = {
droplabAjax: {
endpoint,
method: 'setData',
loadingTemplate: this.loadingTemplate,
},
droplabFilter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol),
},
};
}
itemClicked(e) {
super.itemClicked(e, (selected) => {
const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
});
}
renderContent(forceShowList = false) {
this.droplab
.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
super.renderContent(forceShowList);
}
init() {
this.droplab
.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownNonUser = DropdownNonUser;
})();
/*= require filtered_search/filtered_search_dropdown */
/* global droplabAjaxFilter */
(() => {
class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter);
this.config = {
droplabAjaxFilter: {
endpoint: '/autocomplete/users.json',
searchKey: 'search',
params: {
per_page: 20,
active: true,
project_id: this.getProjectId(),
current_user: true,
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
},
};
}
itemClicked(e) {
super.itemClicked(e,
selected => selected.querySelector('.dropdown-light-content').innerText.trim());
}
renderContent(forceShowList = false) {
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
super.renderContent(forceShowList);
}
getProjectId() {
return this.input.getAttribute('data-project-id');
}
getSearchInput() {
const query = this.input.value.trim();
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
return lastToken.value || '';
}
init() {
this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownUser = DropdownUser;
})();
(() => {
class DropdownUtils {
static getEscapedText(text) {
let escapedText = text;
const hasSpace = text.indexOf(' ') !== -1;
const hasDoubleQuote = text.indexOf('"') !== -1;
// Encapsulate value with quotes if it has spaces
// Known side effect: values's with both single and double quotes
// won't escape properly
if (hasSpace) {
if (hasDoubleQuote) {
escapedText = `'${text}'`;
} else {
// Encapsulate singleQuotes or if it hasSpace
escapedText = `"${text}"`;
}
}
return escapedText;
}
static filterWithSymbol(filterSymbol, item, query) {
const updatedItem = item;
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase();
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1);
}
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
} else {
updatedItem.droplab_hidden = false;
}
return updatedItem;
}
static filterHint(item, query) {
const updatedItem = item;
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
lastToken = lastToken.key || lastToken || '';
if (!lastToken || query.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastToken) {
const split = lastToken.split(':');
const tokenName = split[0].split(' ').last();
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
updatedItem.droplab_hidden = tokenName ? match : false;
}
return updatedItem;
}
static setDataValueIfSelected(filter, selected) {
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
}
// Return boolean based on whether it was set
return dataValue !== null;
}
}
window.gl = window.gl || {};
gl.DropdownUtils = DropdownUtils;
})();
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
/*= require_tree . */
(() => {
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
this.droplab = droplab;
this.hookId = input.getAttribute('data-id');
this.input = input;
this.filter = filter;
this.dropdown = dropdown;
this.loadingTemplate = `<div class="filter-dropdown-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>`;
this.bindEvents();
}
bindEvents() {
this.itemClickedWrapper = this.itemClicked.bind(this);
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
unbindEvents() {
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
}
getCurrentHook() {
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
}
itemClicked(e, getValueFunction) {
const { selected } = e.detail;
if (selected.tagName === 'LI' && selected.innerHTML) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
if (!dataValueSet) {
const value = getValueFunction(selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
}
this.dismissDropdown();
}
}
setAsDropdown() {
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
}
setOffset(offset = 0) {
this.dropdown.style.left = `${offset}px`;
}
renderContent(forceShowList = false) {
if (forceShowList && this.getCurrentHook().list.hidden) {
this.getCurrentHook().list.show();
}
}
render(forceRenderContent = false, forceShowList = false) {
this.setAsDropdown();
const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList);
} else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList);
}
}
dismissDropdown() {
// Focusing on the input will dismiss dropdown
// (default droplab functionality)
this.input.focus();
}
dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open
this.input.dispatchEvent(new Event('input'));
}
hideDropdown() {
this.getCurrentHook().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);
}
}
window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown;
})();
/* global DropLab */
(() => {
class FilteredSearchDropdownManager {
constructor() {
this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchInput = document.querySelector('.filtered-search');
this.setupMapping();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper);
}
cleanup() {
if (this.droplab) {
this.droplab.destroy();
this.droplab = null;
}
this.setupMapping();
document.removeEventListener('page:fetch', this.cleanupWrapper);
}
setupMapping() {
this.mapping = {
author: {
reference: null,
gl: 'DropdownUser',
element: document.querySelector('#js-dropdown-author'),
},
assignee: {
reference: null,
gl: 'DropdownUser',
element: document.querySelector('#js-dropdown-assignee'),
},
milestone: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: ['milestones.json', '%'],
element: document.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: ['labels.json', '~'],
element: document.querySelector('#js-dropdown-label'),
},
hint: {
reference: null,
gl: 'DropdownHint',
element: document.querySelector('#js-dropdown-hint'),
},
};
}
static addWordToInput(tokenName, tokenValue = '') {
const input = document.querySelector('.filtered-search');
const word = `${tokenName}:${tokenValue}`;
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value);
const lastSearchToken = searchToken.split(' ').last();
const lastInputCharacter = input.value[input.value.length - 1];
const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1];
// Remove the typed tokenName
if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') {
// Remove spaces after the colon
if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') {
input.value = input.value.trim();
}
input.value = input.value.slice(0, -1 * lastSearchToken.length);
} else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) {
// Remove the existing tokenValue
const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`;
input.value = input.value.slice(0, -1 * lastTokenString.length);
}
input.value += word;
}
updateCurrentDropdownOffset() {
this.updateDropdownOffset(this.currentDropdown);
}
updateDropdownOffset(key) {
if (!this.font) {
this.font = window.getComputedStyle(this.filteredSearchInput).font;
}
const filterIconPadding = 27;
const offset = gl.text
.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding;
this.mapping[key].reference.setOffset(offset);
}
load(key, firstLoad = false) {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
const element = mappingKey.element;
let forceShowList = false;
if (!mappingKey.reference) {
const dl = this.droplab;
const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
// Passing glArguments to `new gl[glClass](<arguments>)`
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
}
if (firstLoad) {
mappingKey.reference.init();
}
if (this.currentDropdown === 'hint') {
// Force the dropdown to show if it was clicked from the hint dropdown
forceShowList = true;
}
this.updateDropdownOffset(key);
mappingKey.reference.render(firstLoad, forceShowList);
this.currentDropdown = key;
}
loadDropdown(dropdownName = '') {
let firstLoad = false;
if (!this.droplab) {
firstLoad = true;
this.droplab = new DropLab();
}
const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
const key = match && match.key ? match.key : 'hint';
this.load(key, firstLoad);
}
}
setDropdown() {
const { lastToken, searchToken } = this.tokenizer
.processTokens(this.filteredSearchInput.value);
if (this.filteredSearchInput.value.split('').last() === ' ') {
this.updateCurrentDropdownOffset();
}
if (lastToken === searchToken && lastToken !== null) {
// Token is not fully initialized yet because it has no value
// Eg. token = 'label:'
const split = lastToken.split(':');
const dropdownName = split[0].split(' ').last();
this.loadDropdown(split.length > 1 ? dropdownName : '');
} else if (lastToken) {
// Token has been initialized into an object because it has a value
this.loadDropdown(lastToken.key);
} else {
this.loadDropdown('hint');
}
}
resetDropdowns() {
// Force current dropdown to hide
this.mapping[this.currentDropdown].reference.hideDropdown();
// Re-Load dropdown
this.setDropdown();
// Reset filters for current dropdown
this.mapping[this.currentDropdown].reference.resetFilters();
// Reposition dropdown so that it is aligned with cursor
this.updateDropdownOffset(this.currentDropdown);
}
destroyDroplab() {
this.droplab.destroy();
}
}
window.gl = window.gl || {};
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
})();
/* global Turbolinks */
(() => {
class FilteredSearchManager {
constructor() {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager();
this.bindEvents();
this.loadSearchParamsFromURL();
this.dropdownManager.setDropdown();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper);
}
}
cleanup() {
this.unbindEvents();
document.removeEventListener('page:fetch', this.cleanupWrapper);
}
bindEvents() {
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
}
unbindEvents() {
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
}
checkForEnter(e) {
if (e.keyCode === 13) {
e.preventDefault();
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
this.search();
}
}
toggleClearSearchButton(e) {
if (e.target.value) {
this.clearSearchButton.classList.remove('hidden');
} else {
this.clearSearchButton.classList.add('hidden');
}
}
clearSearch(e) {
e.preventDefault();
this.filteredSearchInput.value = '';
this.clearSearchButton.classList.add('hidden');
this.dropdownManager.resetDropdowns();
}
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
const inputValues = [];
params.forEach((p) => {
const split = p.split('=');
const keyParam = decodeURIComponent(split[0]);
const value = split[1];
// Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`);
} else {
// 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);
if (match) {
const indexOf = keyParam.indexOf('_');
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
const symbol = match.symbol;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
}
inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'search') {
inputValues.push(sanitizedValue);
}
}
});
// Trim the last space value
this.filteredSearchInput.value = inputValues.join(' ');
if (inputValues.length > 0) {
this.clearSearchButton.classList.remove('hidden');
}
}
search() {
const paths = [];
const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
const condition = gl.FilteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
if (condition) {
tokenPath = condition.url;
} else {
let tokenValue = token.value;
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
}
paths.push(tokenPath);
});
if (searchToken) {
paths.push(`search=${encodeURIComponent(searchToken)}`);
}
Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
}
}
window.gl = window.gl || {};
gl.FilteredSearchManager = FilteredSearchManager;
})();
(() => {
const tokenKeys = [{
key: 'author',
type: 'string',
param: 'username',
symbol: '@',
}, {
key: 'assignee',
type: 'string',
param: 'username',
symbol: '@',
}, {
key: 'milestone',
type: 'string',
param: 'title',
symbol: '%',
}, {
key: 'label',
type: 'array',
param: 'name[]',
symbol: '~',
}];
const conditions = [{
url: 'assignee_id=0',
tokenKey: 'assignee',
value: 'none',
}, {
url: 'milestone_title=No+Milestone',
tokenKey: 'milestone',
value: 'none',
}, {
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
value: 'upcoming',
}, {
url: 'label_name[]=No+Label',
tokenKey: 'label',
value: 'none',
}];
class FilteredSearchTokenKeys {
static get() {
return tokenKeys;
}
static getConditions() {
return conditions;
}
static searchByKey(key) {
return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
}
static searchBySymbol(symbol) {
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
}
static searchByKeyParam(keyParam) {
return tokenKeys.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`;
}
return keyParam === tokenKeyParam;
}) || null;
}
static searchByConditionUrl(url) {
return conditions.find(condition => condition.url === url) || null;
}
static searchByConditionKeyValue(key, value) {
return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null;
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
})();
(() => {
class FilteredSearchTokenizer {
static processTokens(input) {
// Regex extracts `(token):(symbol)(value)`
// Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g;
const tokens = [];
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
}
return {
tokens,
lastToken,
searchToken,
};
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
})();
......@@ -124,6 +124,12 @@
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
};
gl.utils.getUrlParamsArray = function () {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
return window.location.search.slice(1).split('&');
};
gl.utils.isMetaKey = function(e) {
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
......
......@@ -17,6 +17,21 @@
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
gl.text.getTextWidth = function(text, font) {
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
// re-use canvas object for better performance
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
var context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
};
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
......
......@@ -142,8 +142,9 @@
}
getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
userId = gon.current_user_id;
userName = gon.current_username;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
if (utils.isInGroupsPage() && groupOptions) {
options = groupOptions[utils.getGroupSlug()];
......@@ -158,10 +159,10 @@
header: "" + name
}, {
text: 'Issues assigned to me',
url: issuesPath + "/?assignee_id=" + userId
url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
url: issuesPath + "/?author_id=" + userId
url: issuesPath + "/?author_username=" + userName
}, 'separator', {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId
......
......@@ -73,12 +73,12 @@
<table class="table ci-table">
<thead>
<tr>
<th>Status</th>
<th>Pipeline</th>
<th>Commit</th>
<th>Stages</th>
<th></th>
<th class="hidden-xs"></th>
<th class="pipeline-status">Status</th>
<th class="pipeline-info">Pipeline</th>
<th class="pipeline-commit">Commit</th>
<th class="pipeline-stages">Stages</th>
<th class="pipeline-date"></th>
<th class="pipeline-actions hidden-xs"></th>
</tr>
</thead>
<tbody>
......
......@@ -23,3 +23,118 @@
}
}
.filtered-search-container {
display: -webkit-flex;
display: flex;
}
.filtered-search-input-container {
display: -webkit-flex;
display: flex;
position: relative;
width: 100%;
.form-control {
padding-left: 25px;
padding-right: 25px;
&:focus ~ .fa-filter {
color: $common-gray-dark;
}
}
.fa-filter {
position: absolute;
top: 10px;
left: 10px;
color: $gray-darkest;
}
.fa-times {
right: 10px;
color: $gray-darkest;
}
.clear-search {
width: 35px;
background-color: transparent;
border: none;
position: absolute;
right: 0;
height: 100%;
outline: none;
&:hover .fa-times {
color: $common-gray-dark;
}
}
}
.dropdown-menu .filter-dropdown-item {
padding: 0;
}
.filter-dropdown {
max-height: 215px;
overflow-x: scroll;
}
.filter-dropdown-item {
.btn {
border: none;
width: 100%;
text-align: left;
padding: 8px 16px;
text-overflow: ellipsis;
overflow-y: hidden;
border-radius: 0;
.fa {
width: 15px;
}
.dropdown-label-box {
border-color: $white-light;
border-style: solid;
border-width: 1px;
width: 17px;
height: 17px;
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
}
}
.dropdown-light-content {
font-size: 14px;
font-weight: 400;
}
.dropdown-user {
display: -webkit-flex;
display: flex;
}
.dropdown-user-details {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
}
.hint-dropdown {
width: 250px;
}
.filter-dropdown-loading {
padding: 8px 16px;
}
......@@ -23,12 +23,16 @@
margin-right: 0;
}
.issues-details-filters,
.issues-details-filters:not(.filtered-search-block),
.dash-projects-filters,
.check-all-holder {
display: none;
}
.issues-holder .issue-check {
display: none;
}
.rss-btn {
display: none;
}
......
......@@ -269,6 +269,11 @@ $dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
* Filtered Search
*/
$dropdown-hover-color: #3b86ff;
/*
* Buttons
*/
......
......@@ -110,6 +110,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:html_emails_enabled,
:koding_enabled,
:koding_url,
:plantuml_enabled,
:plantuml_url,
:max_artifacts_size,
:max_attachment_size,
:metrics_enabled,
......
......@@ -166,31 +166,53 @@ class IssuableFinder
end
end
def assignee?
params[:assignee_id].present?
def assignee_id?
params[:assignee_id].present? && params[:assignee_id] != NONE
end
def assignee_username?
params[:assignee_username].present? && params[:assignee_username] != NONE
end
def no_assignee?
# Assignee_id takes precedence over assignee_username
params[:assignee_id] == NONE || params[:assignee_username] == NONE
end
def assignee
return @assignee if defined?(@assignee)
@assignee =
if assignee? && params[:assignee_id] != NONE
User.find(params[:assignee_id])
if assignee_id?
User.find_by(id: params[:assignee_id])
elsif assignee_username?
User.find_by(username: params[:assignee_username])
else
nil
end
end
def author?
params[:author_id].present?
def author_id?
params[:author_id].present? && params[:author_id] != NONE
end
def author_username?
params[:author_username].present? && params[:author_username] != NONE
end
def no_author?
# author_id takes precedence over author_username
params[:author_id] == NONE || params[:author_username] == NONE
end
def author
return @author if defined?(@author)
@author =
if author? && params[:author_id] != NONE
User.find(params[:author_id])
if author_id?
User.find_by(id: params[:author_id])
elsif author_username?
User.find_by(username: params[:author_username])
else
nil
end
......@@ -264,16 +286,24 @@ class IssuableFinder
end
def by_assignee(items)
if assignee?
items = items.where(assignee_id: assignee.try(:id))
if assignee
items = items.where(assignee_id: assignee.id)
elsif no_assignee?
items = items.where(assignee_id: nil)
elsif assignee_id? || assignee_username? # assignee not found
items = items.none
end
items
end
def by_author(items)
if author?
items = items.where(author_id: author.try(:id))
if author
items = items.where(author_id: author.id)
elsif no_author?
items = items.where(author_id: nil)
elsif author_id? || author_username? # author not found
items = items.none
end
items
......
......@@ -247,7 +247,9 @@ module ApplicationHelper
scope: params[:scope],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
assignee_username: params[:assignee_username],
author_id: params[:author_id],
author_username: params[:author_username],
search: params[:search],
label_name: params[:label_name]
}
......
......@@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :koding_enabled
validates :plantuml_url,
presence: true,
if: :plantuml_enabled
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......@@ -196,6 +200,8 @@ class ApplicationSetting < ActiveRecord::Base
akismet_enabled: false,
koding_enabled: false,
koding_url: nil,
plantuml_enabled: false,
plantuml_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
......
......@@ -31,7 +31,7 @@ class CycleAnalytics
repository = @project.repository.raw_repository
sha = @project.repository.commit(ref).sha
cmd = %W(git --git-dir=#{repository.path} log)
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log)
cmd << '--format=%H'
cmd << "--after=#{@from.iso8601}"
cmd << sha
......
......@@ -60,6 +60,8 @@ class Project < ActiveRecord::Base
after_validation :check_pending_delete
after_validation :check_pending_delete
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
......
......@@ -469,6 +469,23 @@
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
%fieldset
%legend PlantUML
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :plantuml_enabled do
= f.check_box :plantuml_enabled
Enable PlantUML
.form-group
= f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
.help-block
Allow rendering of
= link_to "PlantUML", "http://plantuml.com"
diagrams in Asciidoc documents using an external PlantUML service.
%fieldset
%legend Usage statistics
.form-group
......
......@@ -6,6 +6,9 @@
= content_for :sub_nav do
= render "projects/issues/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('filtered_search/filtered_search_bundle.js')
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
......@@ -20,7 +23,6 @@
= icon('rss')
%span.icon-label
Subscribe
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
......@@ -30,7 +32,7 @@
title: "New Issue",
id: "new_issue_link" do
New Issue
= render 'shared/issuable/filter', type: :issues
= render 'shared/issuable/search_bar', type: :issues
.issues-holder
= render 'issues'
......
- type = local_assigns.fetch(:type)
.issues-filters
.issues-details-filters.row-content-block.second-block.filtered-search-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
%input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => '' }
%button.btn.btn-link
= icon('search')
%span
Keep typing and press Enter
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
%button.btn.btn-link
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%i.fa{ class: "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-author.dropdown-menu
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
%img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-assignee.dropdown-menu
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
No Assignee
%li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
%img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
No Milestone
%li.filter-dropdown-item{ 'data-value' => 'upcoming' }
%button.btn.btn-link
Upcoming
%li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
{{title}}
#js-dropdown-label.dropdown-menu{ 'data-dropdown' => true }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
No Label
%li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
%button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
.pull-right
= render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: { id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
:javascript
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
---
title: Scroll to bottom on build completion if autoscroll was active
merge_request: 8391
author:
---
title: Fixes pipeline status cell is too wide by adding missing classes in table head cells
merge_request: 8549
author:
---
title: Search bar redesign first iteration
merge_request: 7345
author:
---
title: Add support for PlantUML diagrams in AsciiDoc documents.
merge_request: 7810
author: Horacio Sanson
---
title: 'Copy <some text> to clipboard'
merge_request: 8535
---
title: Fill missing authorized projects rows
merge_request:
author:
---
title: Remove extra orphaned rows when removing stray namespaces
merge_request: 7841
author:
---
title: Don't validate environment urls on .gitlab-ci.yml
merge_request:
author:
......@@ -111,6 +111,7 @@ module Gitlab
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "terminal/terminal_bundle.js"
config.assets.precompile << "filtered_search/filtered_search_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
......
......@@ -5,47 +5,87 @@ class RemoveUndeletedGroups < ActiveRecord::Migration
DOWNTIME = false
def up
is_ee = defined?(Gitlab::License)
if is_ee
execute <<-EOF.strip_heredoc
DELETE FROM path_locks
WHERE project_id IN (
SELECT project_id
FROM projects
WHERE namespace_id IN (#{namespaces_pending_removal})
);
EOF
execute <<-EOF.strip_heredoc
DELETE FROM remote_mirrors
WHERE project_id IN (
SELECT project_id
FROM projects
WHERE namespace_id IN (#{namespaces_pending_removal})
);
EOF
end
execute <<-EOF.strip_heredoc
DELETE FROM projects
WHERE namespace_id IN (
SELECT id FROM (
SELECT id
FROM namespaces
WHERE deleted_at IS NOT NULL
) namespace_ids
DELETE FROM lists
WHERE label_id IN (
SELECT id
FROM labels
WHERE group_id IN (#{namespaces_pending_removal})
);
EOF
execute <<-EOF.strip_heredoc
DELETE FROM lists
WHERE board_id IN (
SELECT id
FROM boards
WHERE project_id IN (
SELECT project_id
FROM projects
WHERE namespace_id IN (#{namespaces_pending_removal})
)
);
EOF
if defined?(Gitlab::License)
execute <<-EOF.strip_heredoc
DELETE FROM labels
WHERE group_id IN (#{namespaces_pending_removal});
EOF
execute <<-EOF.strip_heredoc
DELETE FROM boards
WHERE project_id IN (
SELECT project_id
FROM projects
WHERE namespace_id IN (#{namespaces_pending_removal})
)
EOF
execute <<-EOF.strip_heredoc
DELETE FROM projects
WHERE namespace_id IN (#{namespaces_pending_removal});
EOF
if is_ee
# EE adds these columns but we have to make sure this data is cleaned up
# here before we run the DELETE below. An alternative would be patching
# this migration in EE but this will only result in a mess and confusing
# migrations.
execute <<-EOF.strip_heredoc
DELETE FROM protected_branch_push_access_levels
WHERE group_id IN (
SELECT id FROM (
SELECT id
FROM namespaces
WHERE deleted_at IS NOT NULL
) namespace_ids
);
WHERE group_id IN (#{namespaces_pending_removal});
EOF
execute <<-EOF.strip_heredoc
DELETE FROM protected_branch_merge_access_levels
WHERE group_id IN (
SELECT id FROM (
SELECT id
FROM namespaces
WHERE deleted_at IS NOT NULL
) namespace_ids
);
WHERE group_id IN (#{namespaces_pending_removal});
EOF
end
# This removes namespaces that were supposed to be soft deleted but still
# reside in the database.
# This removes namespaces that were supposed to be deleted but still reside
# in the database.
execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;"
end
......@@ -54,4 +94,12 @@ class RemoveUndeletedGroups < ActiveRecord::Migration
# If someone is trying to rollback for other reasons, we should not throw an Exception.
# raise ActiveRecord::IrreversibleMigration
end
def namespaces_pending_removal
"SELECT id FROM (
SELECT id
FROM namespaces
WHERE deleted_at IS NOT NULL
) namespace_ids"
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPlantUmlUrlToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :plantuml_url, :string
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :plantuml_enabled, :boolean
end
end
......@@ -14,9 +14,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
namespace_id = user['namespace_id']
path_was = user['username']
path_was_wildcard = quote_string("#{path_was}/%")
path = quote_string(rename_path(path_was))
move_namespace(namespace_id, path_was, path)
path = move_namespace(namespace_id, path_was, path)
execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}"
execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}"
......@@ -45,9 +44,13 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end
def path_exists?(repository_storage_path, path)
gitlab_shell.exists?(repository_storage_path, path)
end
# Accepts invalid path like test.git and returns test_git or
# test_git1 if test_git already taken
def rename_path(path)
def rename_path(repository_storage_path, path)
# To stay closer with original name and reduce risk of duplicates
# we rename suffix instead of removing it
path = path.sub(/\.git\z/, '_git')
......@@ -55,7 +58,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
counter = 0
base = path
while route_exists?(path)
while route_exists?(path) || path_exists?(repository_storage_path, path)
counter += 1
path = "#{base}#{counter}"
end
......@@ -73,6 +76,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
# Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was)
path = quote_string(rename_path(repository_storage_path, path_was))
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
......@@ -83,5 +88,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
path
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class FillAuthorizedProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
class User < ActiveRecord::Base
self.table_name = 'users'
end
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# We're not inserting any data so we don't need to start a transaction.
disable_ddl_transaction!
def up
relation = User.select(:id).
where('authorized_projects_populated IS NOT TRUE')
relation.find_in_batches(batch_size: 1_000) do |rows|
args = rows.map { |row| [row.id] }
Sidekiq::Client.push_bulk('class' => 'AuthorizedProjectsWorker', 'args' => args)
end
end
def down
end
end
......@@ -116,6 +116,8 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
t.boolean "html_emails_enabled", default: true
t.string "plantuml_url"
t.boolean "plantuml_enabled"
end
create_table "approvals", force: :cascade do |t|
......
# PlantUML & GitLab
> [Introduced][ce-7810] in GitLab 8.16.
When [PlantUML](http://plantuml.com) integration is enabled and configured in
GitLab we are able to create simple diagrams in AsciiDoc documents created in
snippets, wikis, and repos.
## PlantUML Server
Before you can enable PlantUML in GitLab; you need to set up your own PlantUML
server that will generate the diagrams. Installing and configuring your
own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
First you need to create a `plantuml.war` file from the source code:
```
sudo apt-get install graphviz openjdk-7-jdk git-core maven
git clone https://github.com/plantuml/plantuml-server.git
cd plantuml-server
mvn package
```
The above sequence of commands will generate a WAR file that can be deployed
using Tomcat:
```
sudo apt-get install tomcat7
sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war
sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war
sudo service restart tomcat7
```
Once the Tomcat service restarts the PlantUML service will be ready and
listening for requests on port 8080:
```
http://localhost:8080/plantuml
```
you can change these defaults by editing the `/etc/tomcat7/server.xml` file.
## GitLab
You need to enable PlantUML integration from Settings under Admin Area. To do
that, login with an Admin account and do following:
- in GitLab go to **Admin Area** and then **Settings**
- scroll to bottom of the page until PlantUML section
- check **Enable PlantUML** checkbox
- set the PlantUML instance as **PlantUML URL**
## Creating Diagrams
With PlantUML integration enabled and configured, we can start adding diagrams to
our AsciiDoc snippets, wikis and repos using blocks:
```
[plantuml, format="png", id="myDiagram", width="200px"]
--
Bob->Alice : hello
Alice -> Bob : Go Away
--
```
The above block will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
render a nice diagram instead of the block:
![PlantUML Integration](../img/integration/plantuml-example.png)
Inside the block you can add any of the supported diagrams by PlantUML such as
[Sequence](http://plantuml.com/sequence-diagram), [Use Case](http://plantuml.com/use-case-diagram),
[Class](http://plantuml.com/class-diagram), [Activity](http://plantuml.com/activity-diagram-legacy),
[Component](http://plantuml.com/component-diagram), [State](http://plantuml.com/state-diagram),
and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML
diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block.
Some parameters can be added to the block definition:
- *format*: Can be either `png` or `svg`. Note that `svg` is not supported by
all browsers so use with care. The default is `png`.
- *id*: A CSS id added to the diagram HTML tag.
- *width*: Width attribute added to the img tag.
- *height*: Height attribute added to the img tag.
......@@ -44,7 +44,9 @@ Example response:
"repository_storage": "default",
"repository_storages": ["default"],
"koding_enabled": false,
"koding_url": null
"koding_url": null,
"plantuml_enabled": false,
"plantuml_url": null
}
```
......@@ -87,6 +89,8 @@ PUT /application/settings
| `elasticsearch_port` | integer | no | The TCP/IP port that Elasticsearch listens to. The default value is 9200 |
| `usage_ping_enabled` | boolean | no | Every week GitLab will report license usage back to GitLab, Inc.|
| `repository_size_limit` | integer | no | Size limit per repository (MB) |
| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
......@@ -119,6 +123,8 @@ Example response:
"container_registry_token_expire_delay": 5,
"repository_storage": "default",
"koding_enabled": false,
"koding_url": null
"koding_url": null,
"plantuml_enabled": false,
"plantuml_url": null
}
```
......@@ -18,6 +18,7 @@ See the documentation below for details on how to configure these services.
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
......
@admin
Feature: Admin Users
Background:
Given I sign in as an admin
And system has users
Scenario: On Admin Users
Given I visit admin users page
Then I should see all users
Scenario: Edit user and change username to non ascii char
When I visit admin users page
And Click edit
And Input non ascii char in username
And Click save
Then See username error message
And Not changed form action url
Scenario: Show user attributes
Given user "Mike" with groups and projects
Given I visit admin users page
And click on "Mike" link
Then I should see user "Mike" details
Scenario: Edit my user attributes
Given I visit admin users page
And click edit on my user
When I submit modified user
Then I see user attributes changed
@javascript
Scenario: Remove users secondary email
Given I visit admin users page
And I view the user with secondary email
And I see the secondary email
When I click remove secondary email
Then I should not see secondary email anymore
Scenario: Show user keys
Given user "Pete" with ssh keys
And I visit admin users page
And click on user "Pete"
And click on ssh keys tab
Then I should see key list
And I click on the key title
Then I should see key details
And I click on remove key
Then I should see the key removed
Scenario: Show user identities
Given user "Pete" with twitter account
And I visit "Pete" identities page in admin
Then I should see twitter details
Scenario: Update user identities
Given user "Pete" with twitter account
And I visit "Pete" identities page in admin
And I modify twitter identity
Then I should see twitter details updated
Scenario: Remove user identities
Given user "Pete" with twitter account
And I visit "Pete" identities page in admin
And I remove twitter identity
Then I should not see twitter details
Scenario: Add note to user attributes
Given I visit admin users page
And click edit on my user
When I submit a note
Then I see note tooltip
@dashboard
Feature: Dashboard Active Tab
Background:
Given I sign in as a user
Scenario: On Dashboard Home
Given I visit dashboard page
Then the active main tab should be Home
And no other main tabs should be active
Scenario: On Dashboard Issues
Given I visit dashboard issues page
Then the active main tab should be Issues
And no other main tabs should be active
Scenario: On Dashboard Merge Requests
Given I visit dashboard merge requests page
Then the active main tab should be Merge Requests
And no other main tabs should be active
Scenario: On Dashboard Groups
Given I visit dashboard groups page
Then the active main tab should be Groups
And no other main tabs should be active
@dashboard
Feature: Dashboard Archived Projects
Background:
Given I sign in as a user
And I own project "Shop"
And I own project "Forum"
And project "Forum" is archived
And I visit dashboard page
Scenario: I should see non-archived projects on dashboard
Then I should see "Shop" project link
And I should not see "Forum" project link
Scenario: I toggle show of archived projects on dashboard
When I click "Show archived projects" link
Then I should see "Shop" project link
And I should see "Forum" project link
@dashboard
Feature: Dashboard Group
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
Scenario: Create a group from dasboard
And I visit dashboard groups page
And I click new group link
And submit form with new group "Samurai" info
Then I should be redirected to group "Samurai" page
And I should see newly created group "Samurai"
@dashboard
Feature: Dashboard Help
Background:
Given I sign in as a user
And I visit the "Rake Tasks" help page
Scenario: The markdown should be rendered correctly
Then I should see "Rake Tasks" page markdown rendered
And Header "Rebuild project satellites" should have correct ids and links
@project_issues
Feature: Project Issues Filter Labels
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" has labels: "bug", "feature", "enhancement"
And project "Shop" has issue "Bugfix1" with labels: "bug", "feature"
And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"
And project "Shop" has issue "Feature1" with labels: "feature"
Given I visit project "Shop" issues page
@javascript
Scenario: I filter by one label
Given I click link "bug"
And I click "dropdown close button"
Then I should see "Bugfix1" in issues list
And I should see "Bugfix2" in issues list
And I should not see "Feature1" in issues list
# TODO: make labels filter works according to this scanario
# right now it looks for label 1 OR label 2. Old behaviour (this test) was
# all issues that have both label 1 AND label 2
#Scenario: I filter by two labels
#Given I click link "bug"
#And I click link "feature"
#Then I should see "Bugfix1" in issues list
#And I should not see "Bugfix2" in issues list
#And I should not see "Feature1" in issues list
......@@ -26,12 +26,6 @@ Feature: Project Issues
Given I click link "Release 0.4"
Then I should see issue "Release 0.4"
@javascript
Scenario: I filter by author
Given I add a user to project "Shop"
And I click "author" dropdown
Then I see current user as the first user
Scenario: I submit new unassigned issue
Given I click link "New Issue"
And I submit new issue "500 error on profile"
......@@ -84,56 +78,6 @@ Feature: Project Issues
And I sort the list by "Least popular"
Then The list should be sorted by "Least popular"
@javascript
Scenario: I search issue
Given I fill in issue search with "Re"
Then I should see "Release 0.4" in issues
And I should not see "Release 0.3" in issues
And I should not see "Tweet control" in issues
@javascript
Scenario: I search issue that not exist
Given I fill in issue search with "Bu"
Then I should not see "Release 0.4" in issues
And I should not see "Release 0.3" in issues
@javascript
Scenario: I search all issues
Given I click link "All"
And I fill in issue search with ".3"
Then I should see "Release 0.3" in issues
And I should not see "Release 0.4" in issues
@javascript
Scenario: Search issues when search string exactly matches issue description
Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
And I fill in issue search with 'Description for issue1'
Then I should see 'Bugfix1' in issues
And I should not see "Release 0.4" in issues
And I should not see "Release 0.3" in issues
And I should not see "Tweet control" in issues
@javascript
Scenario: Search issues when search string partially matches issue description
Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
And project 'Shop' has issue 'Feature1' with description: 'Feature submitted for issue1'
And I fill in issue search with 'issue1'
Then I should see 'Feature1' in issues
Then I should see 'Bugfix1' in issues
And I should not see "Release 0.4" in issues
And I should not see "Release 0.3" in issues
And I should not see "Tweet control" in issues
@javascript
Scenario: Search issues when search string matches no issue description
Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
And I fill in issue search with 'Rock and roll'
Then I should not see 'Bugfix1' in issues
And I should not see "Release 0.4" in issues
And I should not see "Release 0.3" in issues
And I should not see "Tweet control" in issues
# Markdown
Scenario: Headers inside the description should have ids generated for them.
......
class Spinach::Features::AdminUsers < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedAdmin
before do
allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
end
after do
allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original
allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original
end
step 'I should see all users' do
User.all.each do |user|
expect(page).to have_content user.name
end
end
step 'Click edit' do
@user = User.first
find("#edit_user_#{@user.id}").click
end
step 'Input non ascii char in username' do
fill_in 'user_username', with: "\u3042\u3044"
end
step 'Click save' do
click_button("Save")
end
step 'See username error message' do
page.within "#error_explanation" do
expect(page).to have_content "Username"
end
end
step 'Not changed form action url' do
expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"])
end
step 'I submit modified user' do
check :user_can_create_group
click_button 'Save'
end
step 'I see user attributes changed' do
expect(page).to have_content 'Can create groups: Yes'
end
step 'click edit on my user' do
find("#edit_user_#{current_user.id}").click
end
step 'I view the user with secondary email' do
@user_with_secondary_email = User.last
@user_with_secondary_email.emails.new(email: "secondary@example.com")
@user_with_secondary_email.save
visit "/admin/users/#{@user_with_secondary_email.username}"
end
step 'I see the secondary email' do
expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}"
end
step 'I click remove secondary email' do
find("#remove_email_#{@user_with_secondary_email.emails.last.id}").click
end
step 'I should not see secondary email anymore' do
expect(page).not_to have_content "Secondary email:"
end
step 'user "Mike" with groups and projects' do
user = create(:user, name: 'Mike')
project = create(:empty_project)
project.team << [user, :developer]
group = create(:group)
group.add_developer(user)
end
step 'click on "Mike" link' do
click_link "Mike"
end
step 'I should see user "Mike" details' do
expect(page).to have_content 'Account'
expect(page).to have_content 'Personal projects limit'
end
step 'user "Pete" with ssh keys' do
user = create(:user, name: 'Pete')
create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1")
create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2")
end
step 'click on user "Pete"' do
click_link 'Pete'
end
step 'I should see key list' do
expect(page).to have_content 'ssh-rsa Key2'
expect(page).to have_content 'ssh-rsa Key1'
end
step 'I click on the key title' do
click_link 'ssh-rsa Key2'
end
step 'I should see key details' do
expect(page).to have_content 'ssh-rsa Key2'
expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2'
end
step 'I click on remove key' do
click_link 'Remove'
end
step 'I should see the key removed' do
expect(page).not_to have_content 'ssh-rsa Key2'
end
step 'user "Pete" with twitter account' do
@user = create(:user, name: 'Pete')
@user.identities.create!(extern_uid: '123456', provider: 'twitter')
end
step 'I visit "Pete" identities page in admin' do
visit admin_user_identities_path(@user)
end
step 'I should see twitter details' do
expect(page).to have_content 'Pete'
expect(page).to have_content 'twitter'
end
step 'I modify twitter identity' do
find('.table').find(:link, 'Edit').click
fill_in 'identity_extern_uid', with: '654321'
select 'twitter_updated', from: 'identity_provider'
click_button 'Save changes'
end
step 'I should see twitter details updated' do
expect(page).to have_content 'Pete'
expect(page).to have_content 'twitter_updated'
expect(page).to have_content '654321'
end
step 'I remove twitter identity' do
click_link 'Delete'
end
step 'I should not see twitter details' do
expect(page).to have_content 'Pete'
expect(page).not_to have_content 'twitter'
end
step 'click on ssh keys tab' do
click_link 'SSH keys'
end
step 'I submit a note' do
@note = 'The reason to change status'
fill_in 'Note', with: @note
click_button 'Save'
end
step 'I see note tooltip' do
visit admin_users_path
expect(find(".user-note")["title"]).to have_content(@note)
end
end
class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedSidebarActiveTab
end
class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
When 'project "Forum" is archived' do
project = Project.find_by(name: "Forum")
project.update_attribute(:archived, true)
end
step 'I should see "Shop" project link' do
expect(page).to have_link "Shop"
end
step 'I should not see "Forum" project link' do
expect(page).not_to have_link "Forum"
end
step 'I should see "Forum" project link' do
expect(page).to have_link "Forum"
end
step 'I click "Show archived projects" link' do
click_link "Show archived projects"
end
end
class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
include SharedAuthentication
include SharedGroup
include SharedPaths
include SharedUser
step 'I click new group link' do
click_link "New Group"
end
step 'submit form with new group "Samurai" info' do
fill_in 'group_path', with: 'Samurai'
fill_in 'group_description', with: 'Tokugawa Shogunate'
click_button "Create group"
end
step 'I should be redirected to group "Samurai" page' do
expect(current_path).to eq group_path(Group.find_by(name: 'Samurai'))
end
step 'I should see newly created group "Samurai"' do
expect(page).to have_content "Samurai"
expect(page).to have_content "Tokugawa Shogunate"
end
end
class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedMarkdown
step 'I visit the help page' do
visit help_path
end
step 'I visit the "Rake Tasks" help page' do
visit help_page_path("administration/raketasks/maintenance")
end
step 'I should see "Rake Tasks" page markdown rendered' do
expect(page).to have_content "Gather information about GitLab and the system it runs on"
end
step 'Header "Rebuild project satellites" should have correct ids and links' do
header_should_have_correct_id_and_link(2, 'Check GitLab configuration', 'check-gitlab-configuration', '.documentation')
end
end
......@@ -608,6 +608,8 @@ module API
expose :repository_storages
expose :koding_enabled
expose :koding_url
expose :plantuml_enabled
expose :plantuml_url
end
class Release < Grape::Entity
......
......@@ -93,6 +93,10 @@ module API
given koding_enabled: ->(val) { val } do
requires :koding_url, type: String, desc: 'The Koding team URL'
end
optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
given plantuml_enabled: ->(val) { val } do
requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
end
optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
......@@ -128,7 +132,7 @@ module API
:shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay,
:metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_checks_enabled, :koding_enabled, :housekeeping_enabled,
:repository_checks_enabled, :koding_enabled, :housekeeping_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
# GitLab-EE specific settings
:help_text, :max_pages_size, :elasticsearch_indexing, :usage_ping_enabled,
......
require 'asciidoctor'
require 'asciidoctor/converter/html5'
require "asciidoctor-plantuml"
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
......@@ -29,6 +30,8 @@ module Gitlab
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
plantuml_setup
html = ::Asciidoctor.convert(input, asciidoc_opts)
html = Banzai.post_process(html, context)
......@@ -36,6 +39,15 @@ module Gitlab
html.html_safe
end
def self.plantuml_setup
Asciidoctor::PlantUml.configure do |conf|
conf.url = ApplicationSetting.current.plantuml_url
conf.svg_enable = ApplicationSetting.current.plantuml_enabled
conf.png_enable = ApplicationSetting.current.plantuml_enabled
conf.txt_enable = false
end
end
class Html5Converter < Asciidoctor::Converter::Html5Converter
extend Asciidoctor::Converter::Config
......
......@@ -33,7 +33,6 @@ module Gitlab
validates :url,
length: { maximum: 255 },
addressable_url: true,
allow_nil: true
validates :action,
......
......@@ -35,6 +35,7 @@ module Gitlab
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
koding_enabled: false,
plantuml_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
......
......@@ -27,7 +27,7 @@ module Gitlab
private
def load_blame
cmd = %W(git --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
# Read in binary mode to ensure ASCII-8BIT
raw_output = IO.popen(cmd, 'rb') {|io| io.read }
output = encode_utf8(raw_output)
......
......@@ -332,7 +332,7 @@ module Gitlab
end
def log_by_shell(sha, options)
cmd = %W(git --git-dir=#{path} log)
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
cmd += %W(-n #{options[:limit].to_i})
cmd += %w(--format=%H)
cmd += %W(--skip=#{options[:offset].to_i})
......@@ -913,7 +913,7 @@ module Gitlab
return []
end
cmd = %W(git --git-dir=#{path} ls-tree)
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
cmd += %w(-r)
cmd += %w(--full-tree)
cmd += %w(--full-name)
......@@ -1108,7 +1108,7 @@ module Gitlab
end
def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n))
git_archive_cmd = %W(git --git-dir=#{path} archive)
git_archive_cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} archive)
# Put files into a directory before archiving
prefix = "#{archive_name(treeish)}/"
......
......@@ -13,6 +13,7 @@ module Gitlab
if current_user
gon.current_user_id = current_user.id
gon.current_username = current_user.username
end
end
end
......
......@@ -3,7 +3,7 @@ namespace :gitlab do
desc "GitLab | Git | Repack"
task repack: :environment do
failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
if failures.empty?
puts "Done".color(:green)
else
......@@ -13,17 +13,17 @@ namespace :gitlab do
desc "GitLab | Git | Run garbage collection on all repos"
task gc: :environment do
failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting")
if failures.empty?
puts "Done".color(:green)
else
output_failures(failures)
end
end
desc "GitLab | Git | Prune all repos"
task prune: :environment do
failures = perform_git_cmd(%W(git prune), "Git Prune")
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune")
if failures.empty?
puts "Done".color(:green)
else
......
......@@ -44,7 +44,7 @@ namespace :gitlab do
),
Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
/(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
]
......
......@@ -12,7 +12,7 @@ describe Dashboard::TodosController do
end
context 'when using pagination' do
let(:last_page) { user.todos.page().total_pages }
let(:last_page) { user.todos.page.total_pages }
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
before do
......
......@@ -41,6 +41,10 @@ FactoryGirl.define do
mirror_user_id { creator_id }
end
trait :archived do
archived true
end
trait :access_requestable do
request_access_enabled true
end
......
require 'spec_helper'
describe "Admin::Users", feature: true do
describe "Admin::Users", feature: true do
include WaitForAjax
before { login_as :admin }
let!(:user) do
create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
end
let!(:current_user) { login_as :admin }
describe "GET /admin/users" do
before do
......@@ -15,8 +19,10 @@ describe "Admin::Users", feature: true do
end
it "has users list" do
expect(page).to have_content(@user.email)
expect(page).to have_content(@user.name)
expect(page).to have_content(current_user.email)
expect(page).to have_content(current_user.name)
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
end
describe 'Two-factor Authentication filters' do
......@@ -40,8 +46,6 @@ describe "Admin::Users", feature: true do
end
it 'counts users who have not enabled 2FA' do
create(:user)
visit admin_users_path
page.within('.filter-two-factor-disabled small') do
......@@ -50,8 +54,6 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have not enabled 2FA' do
user = create(:user)
visit admin_users_path
click_link '2FA Disabled'
......@@ -110,10 +112,10 @@ describe "Admin::Users", feature: true do
describe "GET /admin/users/:id" do
it "has user info" do
visit admin_users_path
click_link @user.name
click_link user.name
expect(page).to have_content(@user.email)
expect(page).to have_content(@user.name)
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
end
describe 'Impersonation' do
......@@ -126,7 +128,7 @@ describe "Admin::Users", feature: true do
end
it 'does not show impersonate button for admin itself' do
visit admin_user_path(@user)
visit admin_user_path(current_user)
expect(page).not_to have_content('Impersonate')
end
......@@ -158,7 +160,7 @@ describe "Admin::Users", feature: true do
it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
......@@ -171,15 +173,15 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
@user.update_attribute(:otp_required_for_login, true)
user.update_attribute(:otp_required_for_login, true)
visit admin_user_path(@user)
visit admin_user_path(user)
expect_two_factor_status('Enabled')
end
it 'shows when disabled' do
visit admin_user_path(@user)
visit admin_user_path(user)
expect_two_factor_status('Disabled')
end
......@@ -194,9 +196,8 @@ describe "Admin::Users", feature: true do
describe "GET /admin/users/:id/edit" do
before do
@simple_user = create(:user)
visit admin_users_path
click_link "edit_user_#{@simple_user.id}"
click_link "edit_user_#{user.id}"
end
it "has user edit page" do
......@@ -214,45 +215,58 @@ describe "Admin::Users", feature: true do
click_button "Save changes"
end
it "shows page with new data" do
it "shows page with new data" do
expect(page).to have_content('bigbang@mail.com')
expect(page).to have_content('Big Bang')
end
it "changes user entry" do
@simple_user.reload
expect(@simple_user.name).to eq('Big Bang')
expect(@simple_user.is_admin?).to be_truthy
expect(@simple_user.password_expires_at).to be <= Time.now
user.reload
expect(user.name).to eq('Big Bang')
expect(user.is_admin?).to be_truthy
expect(user.password_expires_at).to be <= Time.now
end
end
describe 'update username to non ascii char' do
it do
fill_in 'user_username', with: '\u3042\u3044'
click_button('Save')
page.within '#error_explanation' do
expect(page).to have_content('Username')
end
expect(page).to have_selector(%(form[action="/admin/users/#{user.username}"]))
end
end
end
describe "GET /admin/users/:id/projects" do
let(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
before do
@group = create(:group)
@project = create(:project, group: @group)
@simple_user = create(:user)
@group.add_developer(@simple_user)
group.add_developer(user)
visit projects_admin_user_path(@simple_user)
visit projects_admin_user_path(user)
end
it "lists group projects" do
within(:css, '.append-bottom-default + .panel') do
expect(page).to have_content 'Group projects'
expect(page).to have_link @group.name, admin_group_path(@group)
expect(page).to have_link group.name, admin_group_path(group)
end
end
it 'allows navigation to the group details' do
within(:css, '.append-bottom-default + .panel') do
click_link @group.name
click_link group.name
end
within(:css, 'h3.page-title') do
expect(page).to have_content "Group: #{@group.name}"
expect(page).to have_content "Group: #{group.name}"
end
expect(page).to have_content @project.name
expect(page).to have_content project.name
end
it 'shows the group access level' do
......@@ -270,4 +284,99 @@ describe "Admin::Users", feature: true do
expect(page).not_to have_selector('.group_member')
end
end
describe 'show user attributes' do
it do
visit admin_users_path
click_link user.name
expect(page).to have_content 'Account'
expect(page).to have_content 'Personal projects limit'
end
end
describe 'remove users secondary email', js: true do
let!(:secondary_email) do
create :email, email: 'secondary@example.com', user: user
end
it do
visit admin_user_path(user.username)
expect(page).to have_content("Secondary email: #{secondary_email.email}")
find("#remove_email_#{secondary_email.id}").click
expect(page).not_to have_content(secondary_email.email)
end
end
describe 'show user keys' do
let!(:key1) do
create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1")
end
let!(:key2) do
create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2")
end
it do
visit admin_users_path
click_link user.name
click_link 'SSH keys'
expect(page).to have_content(key1.title)
expect(page).to have_content(key2.title)
click_link key2.title
expect(page).to have_content(key2.title)
expect(page).to have_content(key2.key)
click_link 'Remove'
expect(page).not_to have_content(key2.title)
end
end
describe 'show user identities' do
it 'shows user identities' do
visit admin_user_identities_path(user)
expect(page).to have_content(user.name)
expect(page).to have_content('twitter')
end
end
describe 'update user identities' do
before do
allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
end
it 'modifies twitter identity' do
visit admin_user_identities_path(user)
find('.table').find(:link, 'Edit').click
fill_in 'identity_extern_uid', with: '654321'
select 'twitter_updated', from: 'identity_provider'
click_button 'Save changes'
expect(page).to have_content(user.name)
expect(page).to have_content('twitter_updated')
expect(page).to have_content('654321')
end
end
describe 'remove user with identities' do
it 'removes user with twitter identity' do
visit admin_user_identities_path(user)
click_link 'Delete'
expect(page).to have_content(user.name)
expect(page).not_to have_content('twitter')
end
end
end
......@@ -3,7 +3,6 @@ require 'spec_helper'
feature 'Cycle Analytics', feature: true, js: true do
include WaitForAjax
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project) }
......
require 'spec_helper'
RSpec.describe 'Dashboard Active Tab', feature: true do
before do
login_as :user
end
shared_examples 'page has active tab' do |title|
it "#{title} tab" do
expect(page).to have_selector('.nav-sidebar li.active', count: 1)
expect(find('.nav-sidebar li.active')).to have_content(title)
end
end
context 'on dashboard projects' do
before do
visit dashboard_projects_path
end
it_behaves_like 'page has active tab', 'Projects'
end
context 'on dashboard issues' do
before do
visit issues_dashboard_path
end
it_behaves_like 'page has active tab', 'Issues'
end
context 'on dashboard merge requests' do
before do
visit merge_requests_dashboard_path
end
it_behaves_like 'page has active tab', 'Merge Requests'
end
context 'on dashboard groups' do
before do
visit dashboard_groups_path
end
it_behaves_like 'page has active tab', 'Groups'
end
end
require 'spec_helper'
RSpec.describe 'Dashboard Archived Project', feature: true do
let(:user) { create :user }
let(:project) { create :project}
let(:archived_project) { create(:project, :archived) }
before do
project.team << [user, :master]
archived_project.team << [user, :master]
login_as(user)
visit dashboard_projects_path
end
it 'renders non archived projects' do
expect(page).to have_link(project.name)
expect(page).not_to have_link(archived_project.name)
end
it 'renders all projects' do
click_link 'Show archived projects'
expect(page).to have_link(project.name)
expect(page).to have_link(archived_project.name)
end
end
require 'spec_helper'
RSpec.describe 'Dashboard Group', feature: true do
before do
login_as(:user)
end
it 'creates new grpup' do
visit dashboard_groups_path
click_link 'New Group'
fill_in 'group_path', with: 'Samurai'
fill_in 'group_description', with: 'Tokugawa Shogunate'
click_button 'Create group'
expect(current_path).to eq group_path(Group.find_by(name: 'Samurai'))
expect(page).to have_content('Samurai')
expect(page).to have_content('Tokugawa Shogunate')
end
end
require 'spec_helper'
RSpec.describe 'Dashboard Help', feature: true do
before do
login_as(:user)
end
it 'renders correctly markdown' do
visit help_page_path("administration/raketasks/maintenance")
expect(page).to have_content('Gather information about GitLab and the system it runs on')
node = find('.documentation h2 a#user-content-check-gitlab-configuration')
expect(node[:href]).to eq '#check-gitlab-configuration'
expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration'
end
end
require 'rails_helper'
feature 'Issue filtering by Milestone', feature: true do
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
scenario 'filters by no Milestone', js: true do
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::None.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone')
expect(page).to have_css('.issue', count: 1)
end
context 'filters by upcoming milestone', js: true do
it 'does not show issues with no expiry' do
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 0)
end
it 'shows issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 1)
end
it 'does not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 0)
end
end
scenario 'filters by a specific Milestone', js: true do
create(:issue, project: project, milestone: milestone)
create(:issue, project: project)
visit_issues(project)
filter_by_milestone(milestone.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
expect(page).to have_css('.issue', count: 1)
end
context 'when milestone has single quotes in title' do
background do
milestone.update(name: "rock 'n' roll")
end
scenario 'filters by a specific Milestone', js: true do
create(:issue, project: project, milestone: milestone)
create(:issue, project: project)
visit_issues(project)
filter_by_milestone(milestone.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
expect(page).to have_css('.issue', count: 1)
end
end
def visit_issues(project)
visit namespace_project_issues_path(project.namespace, project)
end
def filter_by_milestone(title)
find(".js-milestone-select").click
find(".milestone-filter .dropdown-content a", text: title).click
end
end
require 'rails_helper'
feature 'Issue filtering by Weight', feature: true do
let(:project) { create(:project, :public) }
let(:weight_num) { random_weight }
before(:each) do
create(:issue, project: project, weight: nil)
create(:issue, project: project, weight: weight_num)
create(:issue, project: project, weight: weight_num)
end
scenario 'filters by no Weight', js: true do
visit_issues(project)
filter_by_weight(Issue::WEIGHT_NONE)
expect(page).to have_css('.weight-filter .dropdown-toggle-text', text: Issue::WEIGHT_NONE)
expect(page).to have_css('.issue', count: 1)
end
scenario 'filters by any Weight', js: true do
visit_issues(project)
filter_by_weight(Issue::WEIGHT_ANY)
expect(page).to have_css('.weight-filter .dropdown-toggle-text', text: Issue::WEIGHT_ANY)
expect(page).to have_css('.issue', count: 2)
end
scenario 'filters by a specific Weight', js: true do
visit_issues(project)
filter_by_weight(weight_num)
expect(page).to have_css('.weight-filter .dropdown-toggle-text', text: weight_num)
expect(page).to have_css('.issue', count: 2)
end
scenario 'all weights', js: true do
visit_issues(project)
filter_by_weight(Issue::WEIGHT_ALL)
expect(page).to have_css('.weight-filter .dropdown-toggle-text', text: Issue::WEIGHT_ALL)
expect(page).to have_css('.issue', count: 3)
end
def visit_issues(project)
visit namespace_project_issues_path(project.namespace, project)
end
def filter_by_weight(title)
find('.js-weight-select').click
find('.weight-filter .dropdown-content a', text: title, match: :first).click
end
def random_weight
Issue::WEIGHT_RANGE.to_a.sample
end
end
require 'rails_helper'
describe 'Dropdown assignee', js: true, feature: true do
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
sleep 5
wait_for_ajax
end
end
def dropdown_assignee_size
page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size
end
def click_assignee(text)
find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click
end
before do
project.team << [user, :master]
project.team << [user_john, :master]
project.team << [user_jacob, :master]
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
describe 'behavior' do
it 'opens when the search bar has assignee:' do
filtered_search.set('assignee:')
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click()
expect(page).to have_css(js_dropdown_assignee, visible: false)
end
it 'should show loading indicator when opened' do
filtered_search.set('assignee:')
expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
end
it 'should hide loading indicator when loaded' do
send_keys_to_filtered_search('assignee:')
expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading')
end
it 'should load all the assignees when opened' do
send_keys_to_filtered_search('assignee:')
expect(dropdown_assignee_size).to eq(3)
end
end
describe 'filtering' do
before do
send_keys_to_filtered_search('assignee:')
end
it 'filters by name' do
send_keys_to_filtered_search('j')
expect(dropdown_assignee_size).to eq(2)
end
it 'filters by case insensitive name' do
send_keys_to_filtered_search('J')
expect(dropdown_assignee_size).to eq(2)
end
it 'filters by username with symbol' do
send_keys_to_filtered_search('@ot')
expect(dropdown_assignee_size).to eq(2)
end
it 'filters by case insensitive username with symbol' do
send_keys_to_filtered_search('@OT')
expect(dropdown_assignee_size).to eq(2)
end
it 'filters by username without symbol' do
send_keys_to_filtered_search('ot')
expect(dropdown_assignee_size).to eq(2)
end
it 'filters by case insensitive username without symbol' do
send_keys_to_filtered_search('OT')
expect(dropdown_assignee_size).to eq(2)
end
end
describe 'selecting from dropdown' do
before do
filtered_search.set('assignee:')
end
it 'fills in the assignee username when the assignee has not been filtered' do
click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}")
end
it 'fills in the assignee username when the assignee has been filtered' do
send_keys_to_filtered_search('roo')
click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user.username}")
end
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:none")
end
end
describe 'input has existing content' do
it 'opens assignee dropdown with existing search term' do
filtered_search.set('searchTerm assignee:')
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing author' do
filtered_search.set('author:@user assignee:')
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing label' do
filtered_search.set('label:~bug assignee:')
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
it 'opens assignee dropdown with existing milestone' do
filtered_search.set('milestone:%v1.0 assignee:')
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
end
end
require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_author) { '#js-dropdown-author' }
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
sleep 5
wait_for_ajax
end
end
def dropdown_author_size
page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size
end
def click_author(text)
find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click
end
before do
project.team << [user, :master]
project.team << [user_john, :master]
project.team << [user_jacob, :master]
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
describe 'behavior' do
it 'opens when the search bar has author:' do
filtered_search.set('author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click()
expect(page).to have_css(js_dropdown_author, visible: false)
end
it 'should show loading indicator when opened' do
filtered_search.set('author:')
expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
end
it 'should hide loading indicator when loaded' do
send_keys_to_filtered_search('author:')
expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading')
end
it 'should load all the authors when opened' do
send_keys_to_filtered_search('author:')
expect(dropdown_author_size).to eq(3)
end
end
describe 'filtering' do
before do
filtered_search.set('author')
send_keys_to_filtered_search(':')
end
it 'filters by name' do
send_keys_to_filtered_search('ja')
expect(dropdown_author_size).to eq(1)
end
it 'filters by case insensitive name' do
send_keys_to_filtered_search('Ja')
expect(dropdown_author_size).to eq(1)
end
it 'filters by username with symbol' do
send_keys_to_filtered_search('@ot')
expect(dropdown_author_size).to eq(2)
end
it 'filters by username without symbol' do
send_keys_to_filtered_search('ot')
expect(dropdown_author_size).to eq(2)
end
it 'filters by case insensitive username without symbol' do
send_keys_to_filtered_search('OT')
expect(dropdown_author_size).to eq(2)
end
end
describe 'selecting from dropdown' do
before do
filtered_search.set('author')
send_keys_to_filtered_search(':')
end
it 'fills in the author username when the author has not been filtered' do
click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user_jacob.username}")
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user.username}")
end
end
describe 'input has existing content' do
it 'opens author dropdown with existing search term' do
filtered_search.set('searchTerm author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'opens author dropdown with existing assignee' do
filtered_search.set('assignee:@user author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'opens author dropdown with existing label' do
filtered_search.set('label:~bug author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
it 'opens author dropdown with existing milestone' do
filtered_search.set('milestone:%v1.0 author:')
expect(page).to have_css(js_dropdown_author, visible: true)
end
end
end
require 'rails_helper'
describe 'Dropdown hint', js: true, feature: true do
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_hint) { '#js-dropdown-hint' }
def dropdown_hint_size
page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
end
def click_hint(text)
find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click
end
before do
project.team << [user, :master]
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
describe 'behavior' do
before do
expect(page).to have_css(js_dropdown_hint, visible: false)
filtered_search.click
end
it 'opens when the search bar is first focused' do
expect(page).to have_css(js_dropdown_hint, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click
expect(page).to have_css(js_dropdown_hint, visible: false)
end
end
describe 'filtering' do
it 'does not filter `Keep typing and press Enter`' do
filtered_search.set('randomtext')
expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false)
expect(dropdown_hint_size).to eq(0)
end
it 'filters with text' do
filtered_search.set('a')
expect(dropdown_hint_size).to eq(3)
end
end
describe 'selecting from dropdown with no input' do
before do
filtered_search.click
end
it 'opens the author dropdown when you click on author' do
click_hint('author')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
expect(filtered_search.value).to eq('author:')
end
it 'opens the assignee dropdown when you click on assignee' do
click_hint('assignee')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
expect(filtered_search.value).to eq('assignee:')
end
it 'opens the milestone dropdown when you click on milestone' do
click_hint('milestone')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
expect(filtered_search.value).to eq('milestone:')
end
it 'opens the label dropdown when you click on label' do
click_hint('label')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
expect(filtered_search.value).to eq('label:')
end
end
describe 'selecting from dropdown with some input' do
it 'opens the author dropdown when you click on author' do
filtered_search.set('auth')
click_hint('author')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
expect(filtered_search.value).to eq('author:')
end
it 'opens the assignee dropdown when you click on assignee' do
filtered_search.set('assign')
click_hint('assignee')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
expect(filtered_search.value).to eq('assignee:')
end
it 'opens the milestone dropdown when you click on milestone' do
filtered_search.set('mile')
click_hint('milestone')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
expect(filtered_search.value).to eq('milestone:')
end
it 'opens the label dropdown when you click on label' do
filtered_search.set('lab')
click_hint('label')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
expect(filtered_search.value).to eq('label:')
end
end
end
require 'rails_helper'
describe 'Dropdown label', js: true, feature: true do
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:uppercase_label) { create(:label, project: project, title: 'BUG') }
let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
let!(:special_label) { create(:label, project: project, 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')}
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_label) { '#js-dropdown-label' }
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
sleep 3
wait_for_ajax
sleep 3
end
end
def dropdown_label_size
page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size
end
def click_label(text)
find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click
end
before do
project.team << [user, :master]
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
describe 'behavior' do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
expect(page).to have_css(js_dropdown_label, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click()
expect(page).to have_css(js_dropdown_label, visible: false)
end
it 'should show loading indicator when opened' do
filtered_search.set('label:')
expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true)
end
it 'should hide loading indicator when loaded' do
send_keys_to_filtered_search('label:')
expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading')
end
it 'should load all the labels when opened' do
send_keys_to_filtered_search('label:')
expect(dropdown_label_size).to be > 0
end
end
describe 'filtering' do
before do
filtered_search.set('label')
end
it 'filters by name' do
send_keys_to_filtered_search(':b')
expect(dropdown_label_size).to eq(2)
end
it 'filters by case insensitive name' do
send_keys_to_filtered_search(':B')
expect(dropdown_label_size).to eq(2)
end
it 'filters by name with symbol' do
send_keys_to_filtered_search(':~bu')
expect(dropdown_label_size).to eq(2)
end
it 'filters by case insensitive name with symbol' do
send_keys_to_filtered_search(':~BU')
expect(dropdown_label_size).to eq(2)
end
it 'filters by multiple words' do
send_keys_to_filtered_search(':Hig')
expect(dropdown_label_size).to eq(1)
end
it 'filters by multiple words with symbol' do
send_keys_to_filtered_search(':~Hig')
expect(dropdown_label_size).to eq(1)
end
it 'filters by multiple words containing single quotes' do
send_keys_to_filtered_search(':won\'t')
expect(dropdown_label_size).to eq(1)
end
it 'filters by multiple words containing single quotes with symbol' do
send_keys_to_filtered_search(':~won\'t')
expect(dropdown_label_size).to eq(1)
end
it 'filters by multiple words containing double quotes' do
send_keys_to_filtered_search(':won"t')
expect(dropdown_label_size).to eq(1)
end
it 'filters by multiple words containing double quotes with symbol' do
send_keys_to_filtered_search(':~won"t')
expect(dropdown_label_size).to eq(1)
end
it 'filters by special characters' do
send_keys_to_filtered_search(':^+')
expect(dropdown_label_size).to eq(1)
end
it 'filters by special characters with symbol' do
send_keys_to_filtered_search(':~^+')
expect(dropdown_label_size).to eq(1)
end
end
describe 'selecting from dropdown' do
before do
filtered_search.set('label:')
end
it 'fills in the label name when the label has not been filled' do
click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{bug_label.title}")
end
it 'fills in the label name when the label is partially filled' do
send_keys_to_filtered_search('bu')
click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{bug_label.title}")
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"")
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"")
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'")
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{uppercase_label.title}")
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{special_label.title}")
end
it 'selects `no label`' do
find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:none")
end
end
describe 'input has existing content' do
it 'opens label dropdown with existing search term' do
filtered_search.set('searchTerm label:')
expect(page).to have_css(js_dropdown_label, visible: true)
end
it 'opens label dropdown with existing author' do
filtered_search.set('author:@person label:')
expect(page).to have_css(js_dropdown_label, visible: true)
end
it 'opens label dropdown with existing assignee' do
filtered_search.set('assignee:@person label:')
expect(page).to have_css(js_dropdown_label, visible: true)
end
it 'opens label dropdown with existing label' do
filtered_search.set('label:~urgent label:')
expect(page).to have_css(js_dropdown_label, visible: true)
end
it 'opens label dropdown with existing milestone' do
filtered_search.set('milestone:%v2.0 label:')
expect(page).to have_css(js_dropdown_label, visible: true)
end
end
end
require 'rails_helper'
describe 'Dropdown milestone', js: true, feature: true do
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
let!(:milestone) { create(:milestone, title: 'v1.0', project: project) }
let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) }
let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) }
let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) }
let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) }
let!(:long_milestone) { create(:milestone, 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', project: project) }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_milestone) { '#js-dropdown-milestone' }
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
sleep 3
wait_for_ajax
sleep 3
end
end
def dropdown_milestone_size
page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size
end
def click_milestone(text)
find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click
end
def click_static_milestone(text)
find('#js-dropdown-milestone .filter-dropdown-item', text: text).click
end
before do
project.team << [user, :master]
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
describe 'behavior' do
it 'opens when the search bar has milestone:' do
filtered_search.set('milestone:')
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
it 'closes when the search bar is unfocused' do
find('body').click()
expect(page).to have_css(js_dropdown_milestone, visible: false)
end
it 'should show loading indicator when opened' do
filtered_search.set('milestone:')
expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
end
it 'should hide loading indicator when loaded' do
send_keys_to_filtered_search('milestone:')
expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading')
end
it 'should load all the milestones when opened' do
send_keys_to_filtered_search('milestone:')
expect(dropdown_milestone_size).to be > 0
end
end
describe 'filtering' do
before do
filtered_search.set('milestone')
end
it 'filters by name' do
send_keys_to_filtered_search(':v1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by case insensitive name' do
send_keys_to_filtered_search(':V1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by name with symbol' do
send_keys_to_filtered_search(':%v1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by case insensitive name with symbol' do
send_keys_to_filtered_search(':%V1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by special characters' do
send_keys_to_filtered_search(':(+')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by special characters with symbol' do
send_keys_to_filtered_search(':%(+')
expect(dropdown_milestone_size).to eq(1)
end
end
describe 'selecting from dropdown' do
before do
filtered_search.set('milestone:')
end
it 'fills in the milestone name when the milestone has not been filled' do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
end
it 'fills in the milestone name when the milestone is partially filled' do
send_keys_to_filtered_search('v')
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
end
it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"")
end
it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"")
end
it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'")
end
it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}")
end
it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}")
end
it 'selects `no milestone`' do
click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:none")
end
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:upcoming")
end
end
describe 'input has existing content' do
it 'opens milestone dropdown with existing search term' do
filtered_search.set('searchTerm milestone:')
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
it 'opens milestone dropdown with existing author' do
filtered_search.set('author:@john milestone:')
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
it 'opens milestone dropdown with existing assignee' do
filtered_search.set('assignee:@john milestone:')
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
it 'opens milestone dropdown with existing label' do
filtered_search.set('label:~important milestone:')
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
it 'opens milestone dropdown with existing milestone' do
filtered_search.set('milestone:%100 milestone:')
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
end
end
This diff is collapsed.
This diff is collapsed.
......@@ -169,16 +169,16 @@ describe "Search", feature: true do
find('.dropdown-menu').click_link 'Issues assigned to me'
sleep 2
expect(page).to have_selector('.issues-holder')
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(page).to have_selector('.filtered-search')
expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
end
it 'takes user to her issues page when issues authored is clicked' do
find('.dropdown-menu').click_link "Issues I've created"
sleep 2
expect(page).to have_selector('.issues-holder')
expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
expect(page).to have_selector('.filtered-search')
expect(find('.filtered-search').value).to eq("author:@#{user.username}")
end
it 'takes user to her MR page when MR assigned is clicked' do
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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