Commit ec1556ca authored by Martin Cabrera's avatar Martin Cabrera

Merge branch 'master' into i-#25814-500-error

parents e1302007 3eebca7b
......@@ -99,18 +99,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# 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.0'
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)
......@@ -841,6 +843,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)
......
......@@ -58,6 +58,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)
......
......@@ -84,6 +84,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
......
......@@ -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;
}
......
......@@ -263,6 +263,11 @@ $dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
* Filtered Search
*/
$dropdown-hover-color: #3b86ff;
/*
* Buttons
*/
......
......@@ -67,69 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
:default_projects_limit,
:default_branch_protection,
:signup_enabled,
:signin_enabled,
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
:sign_in_text,
:after_sign_up_text,
:help_page_text,
:home_page_url,
application_setting_params_ce
)
end
def application_setting_params_ce
[
:admin_notification_email,
:after_sign_out_path,
:max_attachment_size,
:session_expire_delay,
:after_sign_up_text,
:akismet_api_key,
:akismet_enabled,
:container_registry_token_expire_delay,
:default_branch_protection,
:default_group_visibility,
:default_project_visibility,
:default_projects_limit,
:default_snippet_visibility,
:default_group_visibility,
:domain_whitelist_raw,
:domain_blacklist_enabled,
:domain_blacklist_raw,
:domain_blacklist_file,
:version_check_enabled,
:admin_notification_email,
:user_oauth_applications,
:user_default_external,
:shared_runners_enabled,
:shared_runners_text,
:domain_blacklist_raw,
:domain_whitelist_raw,
:email_author_in_body,
:enabled_git_access_protocol,
:gravatar_enabled,
:help_page_text,
:home_page_url,
:housekeeping_bitmaps_enabled,
:housekeeping_enabled,
:housekeeping_full_repack_period,
:housekeeping_gc_period,
:housekeeping_incremental_repack_period,
:html_emails_enabled,
:koding_enabled,
:koding_url,
:plantuml_enabled,
:plantuml_url,
:max_artifacts_size,
:max_attachment_size,
:metrics_enabled,
:metrics_host,
:metrics_port,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
:metrics_packet_size,
:metrics_pool_size,
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
:recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key,
:sentry_enabled,
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
:koding_enabled,
:koding_url,
:email_author_in_body,
:html_emails_enabled,
:recaptcha_site_key,
:repository_checks_enabled,
:metrics_packet_size,
:require_two_factor_authentication,
:session_expire_delay,
:sign_in_text,
:signin_enabled,
:signup_enabled,
:sentry_dsn,
:sentry_enabled,
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:enabled_git_access_protocol,
:shared_runners_enabled,
:shared_runners_text,
:sidekiq_throttling_enabled,
:sidekiq_throttling_factor,
:housekeeping_enabled,
:housekeeping_bitmaps_enabled,
:housekeeping_incremental_repack_period,
:housekeeping_full_repack_period,
:housekeeping_gc_period,
:two_factor_grace_period,
:user_default_external,
:user_oauth_applications,
:version_check_enabled,
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: [],
sidekiq_throttling_queues: []
)
]
end
end
......@@ -61,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
params.require(:group).permit(
params.require(:group).permit(group_params_ce)
end
def group_params_ce
[
:avatar,
:description,
:lfs_enabled,
......@@ -69,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController
:path,
:request_access_enabled,
:visibility_level
)
]
end
end
......@@ -161,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController
@user ||= User.find_by!(username: params[:id])
end
def user_params
params.require(:user).permit(
:email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
:projects_limit, :can_create_group, :admin, :key_id, :external
)
end
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)
end
......@@ -177,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController
def default_route
[:admin, @user]
end
def user_params
params.require(:user).permit(user_params_ce)
end
def user_params_ce
[
:admin,
:avatar,
:bio,
:can_create_group,
:color_scheme_id,
:email,
:extern_uid,
:external,
:force_random_password,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
:linkedin,
:name,
:password_expires_at,
:projects_limit,
:provider,
:remember_me,
:skype,
:theme_id,
:twitter,
:username,
:website_url
]
end
end
......@@ -165,31 +165,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
......@@ -263,16 +285,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
......
......@@ -244,7 +244,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 }
......@@ -184,6 +188,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,
......
module ValidAttribute
extend ActiveSupport::Concern
# Checks whether an attribute has failed validation or not
#
# +attribute+ The symbolised name of the attribute i.e :name
def valid_attribute?(attribute)
self.errors.empty? || self.errors.messages[attribute].nil?
end
end
......@@ -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
......
......@@ -12,6 +12,7 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
include ValidAttribute
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
include Routable
......@@ -65,6 +66,8 @@ class Project < ActiveRecord::Base
end
end
after_validation :check_pending_delete
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
......@@ -1320,4 +1323,21 @@ class Project < ActiveRecord::Base
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
end
def check_pending_delete
return if valid_attribute?(:name) && valid_attribute?(:path)
return unless pending_delete_twin
%i[route route.path name path].each do |error|
errors.delete(error)
end
errors.add(:base, "The project is still being deleted. Please try again later.")
end
def pending_delete_twin
return false unless path
Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace)
end
end
......@@ -420,6 +420,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: Add support for PlantUML diagrams in AsciiDoc documents.
merge_request: 7810
author: Horacio Sanson
---
title: Fill missing authorized projects rows
merge_request:
author:
---
title: Remove extra orphaned rows when removing stray namespaces
merge_request: 7841
author:
---
title: Fix project queued for deletion re-creation tooltip
merge_request:
author:
---
title: Don't validate environment urls on .gitlab-ci.yml
merge_request:
author:
......@@ -106,6 +106,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
# 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
......@@ -107,6 +107,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 "audit_events", 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
}
```
......@@ -80,6 +82,8 @@ PUT /application/settings
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
| `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
......@@ -112,6 +116,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
}
```
......@@ -16,6 +16,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].
......
@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.
......
......@@ -565,6 +565,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.'
......@@ -114,7 +118,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_storage, :repository_checks_enabled, :koding_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
:housekeeping_enabled
end
......
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/
)
]
......
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'
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.
require 'rails_helper'
describe 'Search bar', js: true, feature: true do
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
before do
project.team << [user, :master]
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
def get_left_style(style)
left_style = /left:\s\d*[.]\d*px/.match(style)
left_style.to_s.gsub('left: ', '').to_f
end
describe 'clear search button' do
it 'clears text' do
search_text = 'search_text'
filtered_search.set(search_text)
expect(filtered_search.value).to eq(search_text)
find('.filtered-search-input-container .clear-search').click
expect(filtered_search.value).to eq('')
end
it 'hides by default' do
expect(page).to have_css('.clear-search', visible: false)
end
it 'hides after clicked' do
filtered_search.set('a')
find('.filtered-search-input-container .clear-search').click
expect(page).to have_css('.clear-search', visible: false)
end
it 'hides when there is no text' do
filtered_search.set('a')
filtered_search.set('')
expect(page).to have_css('.clear-search', visible: false)
end
it 'shows when there is text' do
filtered_search.set('a')
expect(page).to have_css('.clear-search', visible: true)
end
it 'resets the dropdown hint filter' do
filtered_search.click
original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
filtered_search.set('author')
expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1)
find('.filtered-search-input-container .clear-search').click
filtered_search.click
expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size)
end
it 'resets the dropdown filters' do
filtered_search.set('a')
hint_style = page.find('#js-dropdown-hint')['style']
hint_offset = get_left_style(hint_style)
filtered_search.set('author:')
expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0)
find('.filtered-search-input-container .clear-search').click
filtered_search.click
expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0
expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset)
end
end
end
......@@ -7,25 +7,27 @@ feature 'Issue filtering by Labels', feature: true, js: true do
let!(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
before do
bug = create(:label, project: project, title: 'bug')
feature = create(:label, project: project, title: 'feature')
enhancement = create(:label, project: project, title: 'enhancement')
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:enhancement) { create(:label, project: project, title: 'enhancement') }
let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") }
let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") }
let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") }
issue1 = create(:issue, title: "Bugfix1", project: project)
issue1.labels << bug
before do
mr1.labels << bug
issue2 = create(:issue, title: "Bugfix2", project: project)
issue2.labels << bug
issue2.labels << enhancement
mr2.labels << bug
mr2.labels << enhancement
issue3 = create(:issue, title: "Feature1", project: project)
issue3.labels << feature
mr3.title = "Feature1"
mr3.labels << feature
project.team << [user, :master]
login_as(user)
visit namespace_project_issues_path(project.namespace, project)
visit namespace_project_merge_requests_path(project.namespace, project)
end
context 'filter by label bug' do
......
......@@ -8,81 +8,88 @@ feature 'Issues filter reset button', feature: true, js: true do
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:bug) { create(:label, project: project, name: 'bug')}
let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) }
let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") }
let(:merge_request_css) { '.merge-request' }
before do
mr2.labels << bug
project.team << [user, :developer]
end
context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do
visit_issues(project, milestone_title: milestone.title)
expect(page).to have_css('.issue', count: 1)
visit_merge_requests(project, milestone_title: milestone.title)
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
expect(page).to have_css(merge_request_css, count: 2)
end
end
context 'when a label filter has been applied' do
it 'resets the label filter' do
visit_issues(project, label_name: bug.name)
expect(page).to have_css('.issue', count: 1)
visit_merge_requests(project, label_name: bug.name)
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
expect(page).to have_css(merge_request_css, count: 2)
end
end
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_issues(project, search: 'Bug')
expect(page).to have_css('.issue', count: 1)
visit_merge_requests(project, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
expect(page).to have_css(merge_request_css, count: 2)
end
end
context 'when author filter has been applied' do
it 'resets the author filter' do
visit_issues(project, author_id: user.id)
expect(page).to have_css('.issue', count: 1)
visit_merge_requests(project, author_id: user.id)
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
expect(page).to have_css(merge_request_css, count: 2)
end
end
context 'when assignee filter has been applied' do
it 'resets the assignee filter' do
visit_issues(project, assignee_id: user.id)
expect(page).to have_css('.issue', count: 1)
visit_merge_requests(project, assignee_id: user.id)
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
expect(page).to have_css(merge_request_css, count: 2)
end
end
context 'when all filters have been applied' do
it 'resets all filters' do
visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
expect(page).to have_css('.issue', count: 0)
visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 0)
reset_filters
expect(page).to have_css('.issue', count: 2)
expect(page).to have_css(merge_request_css, count: 2)
end
end
context 'when no filters have been applied' do
it 'the reset link should not be visible' do
visit_issues(project)
expect(page).to have_css('.issue', count: 2)
visit_merge_requests(project)
expect(page).to have_css(merge_request_css, count: 2)
expect(page).not_to have_css '.reset_filters'
end
end
def visit_merge_requests(project, opts = {})
visit namespace_project_merge_requests_path project.namespace, project, opts
end
def reset_filters
find('.reset-filters').click
end
......
......@@ -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
......
//= require extensions/array
//= require filtered_search/dropdown_utils
//= require filtered_search/filtered_search_tokenizer
//= require filtered_search/filtered_search_dropdown_manager
(() => {
describe('Dropdown Utils', () => {
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
expect(escaped).toBe('textWithoutSpace');
});
it('should escape with double quotes', () => {
let escaped = gl.DropdownUtils.getEscapedText('text with space');
expect(escaped).toBe('"text with space"');
escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
expect(escaped).toBe('"won\'t fix"');
});
it('should escape with single quotes', () => {
const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
expect(escaped).toBe('\'won"t fix\'');
});
it('should escape with single quotes by default', () => {
const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
expect(escaped).toBe('\'won"t\' fix\'');
});
});
describe('filterWithSymbol', () => {
const item = {
title: '@root',
};
it('should filter without symbol', () => {
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo');
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with symbol', () => {
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo');
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with colon', () => {
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':');
expect(updatedItem.droplab_hidden).toBe(false);
});
});
describe('filterHint', () => {
it('should filter', () => {
let updatedItem = gl.DropdownUtils.filterHint({
hint: 'label',
}, 'l');
expect(updatedItem.droplab_hidden).toBe(false);
updatedItem = gl.DropdownUtils.filterHint({
hint: 'label',
}, 'o');
expect(updatedItem.droplab_hidden).toBe(true);
});
it('should return droplab_hidden false when item has no hint', () => {
const updatedItem = gl.DropdownUtils.filterHint({}, '');
expect(updatedItem.droplab_hidden).toBe(false);
});
});
describe('setDataValueIfSelected', () => {
beforeEach(() => {
spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
.and.callFake(() => {});
});
it('calls addWordToInput when dataValue exists', () => {
const selected = {
getAttribute: () => 'value',
};
gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
});
it('returns true when dataValue exists', () => {
const selected = {
getAttribute: () => 'value',
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(result).toBe(true);
});
it('returns false when dataValue does not exist', () => {
const selected = {
getAttribute: () => null,
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(result).toBe(false);
});
});
});
})();
//= require extensions/array
//= require filtered_search/filtered_search_tokenizer
//= require filtered_search/filtered_search_dropdown_manager
(() => {
describe('Filtered Search Dropdown Manager', () => {
describe('addWordToInput', () => {
function getInputValue() {
return document.querySelector('.filtered-search').value;
}
function setInputValue(value) {
document.querySelector('.filtered-search').value = value;
}
beforeEach(() => {
const input = document.createElement('input');
input.classList.add('filtered-search');
document.body.appendChild(input);
});
afterEach(() => {
document.querySelector('.filtered-search').outerHTML = '';
});
describe('input has no existing value', () => {
it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone');
expect(getInputValue()).toBe('milestone:');
});
it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
expect(getInputValue()).toBe('label:none');
});
});
describe('input has existing value', () => {
it('should be able to just add tokenName', () => {
setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author');
expect(getInputValue()).toBe('author:');
});
it('should replace tokenValue', () => {
setInputValue('author:roo');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
expect(getInputValue()).toBe('author:@root');
});
it('should add tokenValues containing spaces', () => {
setInputValue('label:~"test');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
expect(getInputValue()).toBe('label:~\'"test me"\'');
});
});
});
});
})();
This diff is collapsed.
......@@ -8,6 +8,10 @@ module Gitlab
let(:html) { 'H<sub>2</sub>O' }
context "without project" do
before do
allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
end
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
......
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