Commit 6856da43 authored by Sean McGivern's avatar Sean McGivern

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

CE Upstream - Monday

See merge request !2325
parents 3e14e718 5982811e
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"gon": false, "gon": false,
"localStorage": false "localStorage": false
}, },
"parser": "babel-eslint",
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
......
...@@ -460,8 +460,6 @@ codeclimate: ...@@ -460,8 +460,6 @@ codeclimate:
services: services:
- docker:dind - docker:dind
script: script:
- docker pull stedolan/jq
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts: artifacts:
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -24,27 +23,9 @@ const categoryLabelMap = { ...@@ -24,27 +23,9 @@ const categoryLabelMap = {
flags: 'Flags', flags: 'Flags',
}; };
function renderCategory(name, emojiList, opts = {}) { class AwardsHandler {
return ` constructor(emoji) {
<h5 class="emoji-menu-title"> this.emoji = emoji;
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
export default class AwardsHandler {
constructor() {
this.eventListeners = []; this.eventListeners = [];
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
...@@ -78,10 +59,10 @@ export default class AwardsHandler { ...@@ -78,10 +59,10 @@ export default class AwardsHandler {
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon'); const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$target.closest('.js-awards-block').addClass('current'); $target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
}); });
} }
...@@ -139,16 +120,16 @@ export default class AwardsHandler { ...@@ -139,16 +120,16 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true; this.isCreatingEmojiMenu = true;
// Render the first category // Render the first category
const categoryMap = Emoji.getEmojiCategoryMap(); const categoryMap = this.emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0]; const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
// Render the frequently used // Render the frequently used
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
let frequentlyUsedCatgegory = ''; let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) { if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
menuListClass: 'frequent-emojis', menuListClass: 'frequent-emojis',
}); });
} }
...@@ -179,7 +160,7 @@ export default class AwardsHandler { ...@@ -179,7 +160,7 @@ export default class AwardsHandler {
} }
this.isAddingRemainingEmojiMenuCategories = true; this.isAddingRemainingEmojiMenuCategories = true;
const categoryMap = Emoji.getEmojiCategoryMap(); const categoryMap = this.emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately // Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive // This will take more time, but makes UI more responsive
...@@ -191,7 +172,7 @@ export default class AwardsHandler { ...@@ -191,7 +172,7 @@ export default class AwardsHandler {
promiseChain.then(() => promiseChain.then(() =>
new Promise((resolve) => { new Promise((resolve) => {
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const categoryMarkup = renderCategory( const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey], categoryLabelMap[categoryNameKey],
emojisInCategory, emojisInCategory,
); );
...@@ -216,6 +197,25 @@ export default class AwardsHandler { ...@@ -216,6 +197,25 @@ export default class AwardsHandler {
}); });
} }
renderCategory(name, emojiList, opts = {}) {
return `
<h5 class="emoji-menu-title">
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
...@@ -234,7 +234,7 @@ export default class AwardsHandler { ...@@ -234,7 +234,7 @@ export default class AwardsHandler {
} }
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
...@@ -249,7 +249,7 @@ export default class AwardsHandler { ...@@ -249,7 +249,7 @@ export default class AwardsHandler {
this.checkMutuality(votesBlock, emoji); this.checkMutuality(votesBlock, emoji);
} }
this.addEmojiToFrequentlyUsedList(emoji); this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) { if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) { if (this.isActive($emojiButton)) {
...@@ -374,7 +374,7 @@ export default class AwardsHandler { ...@@ -374,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = ` const buttonHtml = `
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
${Emoji.glEmojiTag(emojiName)} ${this.emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span> <span class="award-control-text js-counter">1</span>
</button> </button>
`; `;
...@@ -440,7 +440,7 @@ export default class AwardsHandler { ...@@ -440,7 +440,7 @@ export default class AwardsHandler {
} }
addEmojiToFrequentlyUsedList(emoji) { addEmojiToFrequentlyUsedList(emoji) {
if (Emoji.isEmojiNameValid(emoji)) { if (this.emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
} }
...@@ -450,7 +450,7 @@ export default class AwardsHandler { ...@@ -450,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => { return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => Emoji.isEmojiNameValid(inputName), inputName => this.emoji.isEmojiNameValid(inputName),
); );
return this.frequentlyUsedEmojis; return this.frequentlyUsedEmojis;
...@@ -493,7 +493,7 @@ export default class AwardsHandler { ...@@ -493,7 +493,7 @@ export default class AwardsHandler {
} }
findMatchingEmojiElements(query) { findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query); const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
...@@ -507,3 +507,12 @@ export default class AwardsHandler { ...@@ -507,3 +507,12 @@ export default class AwardsHandler {
$('.emoji-menu').remove(); $('.emoji-menu').remove();
} }
} }
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
.then(Emoji => new AwardsHandler(Emoji));
}
return awardsHandlerPromise;
}
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support'; import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window); installCustomElements(window);
...@@ -32,11 +31,19 @@ export default function installGlEmojiElement() { ...@@ -32,11 +31,19 @@ export default function installGlEmojiElement() {
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = emojiFallbackImageSrc(name); import(/* webpackChunkName: 'emoji' */ '../emoji')
this.innerHTML = emojiImageTag(name, src); .then(({ emojiImageTag, emojiFallbackImageSrc }) => {
if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
})
.catch(() => {
// do nothing
});
} }
} }
}; };
......
...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}, },
milestoneTitle() { milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
} },
canRemove() {
return !this.list.preset;
},
}, },
watch: { watch: {
detail: { detail: {
......
...@@ -51,8 +51,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -51,8 +51,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
template: ` template: `
<div <div
class="block list" class="block list">
v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp'; import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache'; import AjaxCache from './lib/utils/ajax_cache';
...@@ -373,7 +372,12 @@ class GfmAutoComplete { ...@@ -373,7 +372,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, validEmojiNames); import(/* webpackChunkName: 'emoji' */ './emoji')
.then(({ validEmojiNames, glEmojiTag }) => {
this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
} else { } else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => { .then((data) => {
...@@ -428,12 +432,14 @@ GfmAutoComplete.atTypeMap = { ...@@ -428,12 +432,14 @@ GfmAutoComplete.atTypeMap = {
}; };
// Emoji // Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = { GfmAutoComplete.Emoji = {
templateFunction(name) { templateFunction(name) {
return `<li> // glEmojiTag helper is loaded on-demand in fetchData()
${name} ${glEmojiTag(name)} if (GfmAutoComplete.glEmojiTag) {
</li> return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
`; }
return `<li>${name}</li>`;
}, },
}; };
// Team Members // Team Members
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
...@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
return navbarHeight + layoutNavHeight + subNavScroll; return navbarHeight + layoutNavHeight + subNavScroll;
} }
initSidebar() {
if (!this.navHeight) {
this.navHeight = this.getNavHeight();
}
if (!this.sidebarInitialized) {
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
this.sidebarInitialized = true;
}
}
setupBulkUpdateActions() { setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData(); IssuableBulkUpdateActions.setOriginalDropdownData();
} }
...@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
this.toggleCheckboxDisplay(enable); this.toggleCheckboxDisplay(enable);
if (enable) { if (enable) {
this.initSidebar(); SidebarHeightManager.init();
} }
} }
...@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.enable(); this.$bulkEditSubmitBtn.enable();
} }
} }
// loosely based on method of the same name in right_sidebar.js
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.navHeight - currentScrollDepth;
if (diff > 0) {
this.$sidebar.outerHeight(window.innerHeight - diff);
} else {
this.$sidebar.outerHeight('100%');
}
}
static getCheckedIssueIds() { static getCheckedIssueIds() {
const $checkedIssues = $('.selected_issue:checked'); const $checkedIssues = $('.selected_issue:checked');
......
...@@ -112,29 +112,11 @@ window.dateFormat = dateFormat; ...@@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
return timefor; return timefor;
}; };
w.gl.utils.cachedTimeagoElements = [];
w.gl.utils.renderTimeago = function($els) { w.gl.utils.renderTimeago = function($els) {
if (!$els && !w.gl.utils.cachedTimeagoElements.length) { const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
} else if ($els) {
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
}
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
};
w.gl.utils.updateTimeagoText = function(el) {
const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang);
if (el.textContent !== formattedDate) {
el.textContent = formattedDate;
}
};
w.gl.utils.initTimeagoTimeout = function() {
gl.utils.renderTimeago();
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); // timeago.js sets timeouts internally for each timeago value to be updated in real time
gl.utils.getTimeago().render(timeagoEls, lang);
}; };
w.gl.utils.getDayDifference = function(a, b) { w.gl.utils.getDayDifference = function(a, b) {
......
...@@ -70,7 +70,7 @@ import './ajax_loading_spinner'; ...@@ -70,7 +70,7 @@ import './ajax_loading_spinner';
import './api'; import './api';
import './aside'; import './aside';
import './autosave'; import './autosave';
import AwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './breakpoints'; import './breakpoints';
import './broadcast_message'; import './broadcast_message';
import './build'; import './build';
...@@ -366,10 +366,10 @@ $(function () { ...@@ -366,10 +366,10 @@ $(function () {
$window.off('resize.app').on('resize.app', function () { $window.off('resize.app').on('resize.app', function () {
return fitSidebarForSize(); return fitSidebarForSize();
}); });
gl.awardsHandler = new AwardsHandler(); loadAwardsHandler();
new Aside(); new Aside();
gl.utils.initTimeagoTimeout(); gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs'); $(document).trigger('init.scrolling-tabs');
}); });
...@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho ...@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave'; import './autosave';
import './dropzone_input'; import './dropzone_input';
import './task_list'; import './task_list';
...@@ -291,8 +292,13 @@ export default class Notes { ...@@ -291,8 +292,13 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) { if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0); votesBlock = $('.js-awards-block').eq(0);
gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
return gl.awardsHandler.scrollToAwards(); loadAwardsHandler().then((awardsHandler) => {
awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
}).catch(() => {
// ignore
});
} }
} }
} }
...@@ -337,6 +343,10 @@ export default class Notes { ...@@ -337,6 +343,10 @@ export default class Notes {
if (!noteEntity.valid) { if (!noteEntity.valid) {
if (noteEntity.errors.commands_only) { if (noteEntity.errors.commands_only) {
if (noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh(); this.refresh();
} }
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import SidebarHeightManager from './sidebar_height_manager';
(function() { (function() {
this.Sidebar = (function() { this.Sidebar = (function() {
...@@ -8,12 +9,6 @@ import Cookies from 'js-cookie'; ...@@ -8,12 +9,6 @@ import Cookies from 'js-cookie';
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside'); this.sidebar = $('aside');
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$layoutNav = $('.layout-nav');
this.$subScroll = $('.sub-nav-scroll');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners(); this.removeListeners();
this.addEventListeners(); this.addEventListeners();
} }
...@@ -27,16 +22,14 @@ import Cookies from 'js-cookie'; ...@@ -27,16 +22,14 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
SidebarHeightManager.init();
const $document = $(document); const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => slowerThrottledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) { $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
...@@ -214,18 +207,6 @@ import Cookies from 'js-cookie'; ...@@ -214,18 +207,6 @@ import Cookies from 'js-cookie';
} }
}; };
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0);
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff);
this.$sidebarInner.height('100%');
} else {
this.$rightSidebar.outerHeight('100%');
this.$sidebarInner.height('');
}
};
Sidebar.prototype.isOpen = function() { Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded'); return this.sidebar.is('.right-sidebar-expanded');
}; };
......
export default {
init() {
if (!this.initialized) {
this.$window = $(window);
this.$rightSidebar = $('.js-right-sidebar');
this.$navHeight = $('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.sub-nav-scroll').outerHeight();
const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
this.$window.on('scroll', throttledSetSidebarHeight);
this.$window.on('resize', debouncedSetSidebarHeight);
this.initialized = true;
}
},
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.$navHeight - currentScrollDepth;
if (diff > 0) {
const newSidebarHeight = window.innerHeight - diff;
this.$rightSidebar.outerHeight(newSidebarHeight);
this.sidebarHeightIsCustom = true;
} else if (this.sidebarHeightIsCustom) {
this.$rightSidebar.outerHeight('100%');
this.sidebarHeightIsCustom = false;
}
},
};
/**
* This is the first script loaded by webpack's runtime. It is used to manually configure
* config.output.publicPath to account for relative_url_root or CDN settings which cannot be
* baked-in to our webpack bundles.
*/
if (gon && gon.webpack_public_path) {
__webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line
}
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
max-width: $limited-layout-width-sm; max-width: $limited-layout-width-sm;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 64px;
padding-bottom: 64px;
} }
} }
......
...@@ -49,6 +49,7 @@ $new-sidebar-width: 220px; ...@@ -49,6 +49,7 @@ $new-sidebar-width: 220px;
position: fixed; position: fixed;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $new-sidebar-width;
transition: width $sidebar-transition-duration;
top: 50px; top: 50px;
bottom: 0; bottom: 0;
left: 0; left: 0;
...@@ -62,6 +63,8 @@ $new-sidebar-width: 220px; ...@@ -62,6 +63,8 @@ $new-sidebar-width: 220px;
} }
li { li {
white-space: nowrap;
a { a {
display: block; display: block;
padding: 12px 14px; padding: 12px 14px;
...@@ -72,6 +75,10 @@ $new-sidebar-width: 220px; ...@@ -72,6 +75,10 @@ $new-sidebar-width: 220px;
color: $gl-text-color; color: $gl-text-color;
text-decoration: none; text-decoration: none;
} }
@media (max-width: $screen-xs-max) {
width: 0;
}
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
......
...@@ -491,11 +491,12 @@ a.deploy-project-label { ...@@ -491,11 +491,12 @@ a.deploy-project-label {
.project-stats { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
max-width: 100%;
border-bottom: 1px solid $border-color;
.nav { .nav {
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid $border-color;
} }
.nav > li { .nav > li {
......
...@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestone_states = GlobalMilestone.states_count(@projects) @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
format.json do
render json: milestones.map { |m| m.for_display.slice(:title, :name) }
end
end end
end end
......
...@@ -165,7 +165,6 @@ module IssuablesHelper ...@@ -165,7 +165,6 @@ module IssuablesHelper
} }
state_title = titles[state] || state.to_s.humanize state_title = titles[state] || state.to_s.humanize
count = issuables_count_for_state(issuable_type, state) count = issuables_count_for_state(issuable_type, state)
html = content_tag(:span, state_title) html = content_tag(:span, state_title)
......
...@@ -74,6 +74,8 @@ module MilestonesHelper ...@@ -74,6 +74,8 @@ module MilestonesHelper
project = @target_project || @project project = @target_project || @project
if project if project
namespace_project_milestones_path(project.namespace, project, :json) namespace_project_milestones_path(project.namespace, project, :json)
elsif @group
group_milestones_path(@group, :json)
else else
dashboard_milestones_path(:json) dashboard_milestones_path(:json)
end end
......
...@@ -11,20 +11,29 @@ module WebpackHelper ...@@ -11,20 +11,29 @@ module WebpackHelper
paths = Webpack::Rails::Manifest.asset_paths(source) paths = Webpack::Rails::Manifest.asset_paths(source)
if extension if extension
paths = paths.select { |p| p.ends_with? ".#{extension}" } paths.select! { |p| p.ends_with? ".#{extension}" }
end end
# include full webpack-dev-server url for rspec tests running locally force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
end
paths
end
def webpack_public_host
if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled
host = Rails.configuration.webpack.dev_server.host host = Rails.configuration.webpack.dev_server.host
port = Rails.configuration.webpack.dev_server.port port = Rails.configuration.webpack.dev_server.port
protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http' protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
"#{protocol}://#{host}:#{port}"
paths.map! do |p| else
"#{protocol}://#{host}:#{port}#{p}" ActionController::Base.asset_host.try(:chomp, '/')
end
end end
end
paths def webpack_public_path
"#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/"
end end
end end
...@@ -14,7 +14,7 @@ module Mentionable ...@@ -14,7 +14,7 @@ module Mentionable
end end
EXTERNAL_PATTERN = begin EXTERNAL_PATTERN = begin
issue_pattern = ExternalIssue.reference_pattern issue_pattern = IssueTrackerService.reference_pattern
link_patterns = URI.regexp(%w(http https)) link_patterns = URI.regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern) reference_pattern(link_patterns, issue_pattern)
end end
......
...@@ -38,11 +38,6 @@ class ExternalIssue ...@@ -38,11 +38,6 @@ class ExternalIssue
@project.id @project.id
end end
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil, full: nil) def to_reference(_from_project = nil, full: nil)
id id
end end
......
...@@ -728,8 +728,8 @@ class Project < ActiveRecord::Base ...@@ -728,8 +728,8 @@ class Project < ActiveRecord::Base
end end
end end
def issue_reference_pattern def external_issue_reference_pattern
issues_tracker.reference_pattern external_issue_tracker.class.reference_pattern
end end
def default_issues_tracker? def default_issues_tracker?
......
...@@ -5,7 +5,10 @@ class IssueTrackerService < Service ...@@ -5,7 +5,10 @@ class IssueTrackerService < Service
# Pattern used to extract links from comments # Pattern used to extract links from comments
# Override this method on services that uses different patterns # Override this method on services that uses different patterns
def reference_pattern # This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
def self.reference_pattern
@reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
end end
......
...@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService ...@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService
end end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end end
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
= Gon::Base.render_data = Gon::Base.render_data
= webpack_bundle_tag "runtime" = webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common" = webpack_bundle_tag "common"
= webpack_bundle_tag "locale" = webpack_bundle_tag "locale"
= webpack_bundle_tag "main" = webpack_bundle_tag "main"
......
...@@ -23,4 +23,5 @@ ...@@ -23,4 +23,5 @@
= render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications" = render "projects/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue", %remove-btn{ ":issue" => "issue",
":list" => "list" } ":list" => "list",
"v-if" => "canRemove" }
...@@ -18,9 +18,6 @@ ...@@ -18,9 +18,6 @@
= render 'projects/last_push' = render 'projects/last_push'
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project
- if @project.merge_requests.exists? - if @project.merge_requests.exists?
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
#{ _('Set up auto deploy') } #{ _('Set up auto deploy') }
%div{ class: container_class } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived? - if @project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
......
- if @issues.to_a.any? - if @issues.to_a.any?
.panel.panel-default.panel-small.panel-without-border .panel.panel-default.panel-small.panel-without-border
%ul.content-list.issues-list %ul.content-list.issues-list.issuable-list
= render partial: 'projects/issues/issue', collection: @issues = render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
- else - else
......
- if @merge_requests.to_a.any? - if @merge_requests.to_a.any?
.panel.panel-default.panel-small.panel-without-border .panel.panel-default.panel-small.panel-without-border
%ul.content-list.mr-list %ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
......
- model_name = source.model_name.to_s.downcase - model_name = source.model_name.to_s.downcase
.project-action-button.inline - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) .project-action-button.inline
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project') - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
= link_to link_text, polymorphic_path([:leave, source, :members]), = link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete, method: :delete,
data: { confirm: leave_confirmation_message(source) }, data: { confirm: leave_confirmation_message(source) },
class: 'btn' class: 'btn'
- elsif requester = source.requesters.find_by(user_id: current_user.id) - elsif requester = source.requesters.find_by(user_id: current_user.id)
.project-action-button.inline
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]), = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
method: :delete, method: :delete,
data: { confirm: remove_member_message(requester) }, data: { confirm: remove_member_message(requester) },
class: 'btn' class: 'btn'
- elsif source.request_access_enabled && can?(current_user, :request_access, source) - elsif source.request_access_enabled && can?(current_user, :request_access, source)
.project-action-button.inline
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]), = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
method: :post, method: :post,
class: 'btn' class: 'btn'
---
title: Replace 'dashboard/new-project.feature' spinach with rspec
merge_request: 12550
author: Alexander Randa (@randaalex)
---
title: Remove "Remove from board" button from backlog and closed list
merge_request: 12430
author:
---
title: Change milestone endpoint for groups
merge_request: 12374
author: Takuya Noguchi
---
title: Improve support for external issue references
merge_request: 12485
author:
---
title: Enable support for webpack code-splitting by dynamically setting publicPath
at runtime
merge_request: 12032
author:
---
title: Add issuable-list class to shared mr/issue lists to fix new responsive layout
design
merge_request:
author:
...@@ -75,6 +75,7 @@ var config = { ...@@ -75,6 +75,7 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js', test: './test.js',
peek: './peek.js', peek: './peek.js',
webpack_runtime: './webpack.js',
}, },
output: { output: {
...@@ -197,7 +198,7 @@ var config = { ...@@ -197,7 +198,7 @@ var config = {
// create cacheable common library bundles // create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({ new webpack.optimize.CommonsChunkPlugin({
names: ['main', 'locale', 'common', 'runtime'], names: ['main', 'locale', 'common', 'webpack_runtime'],
}), }),
], ],
...@@ -252,7 +253,6 @@ if (IS_DEV_SERVER) { ...@@ -252,7 +253,6 @@ if (IS_DEV_SERVER) {
hot: DEV_SERVER_LIVERELOAD, hot: DEV_SERVER_LIVERELOAD,
inline: DEV_SERVER_LIVERELOAD inline: DEV_SERVER_LIVERELOAD
}; };
config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath;
config.plugins.push( config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error // watch node_modules for changes if we encounter a missing module compile error
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
......
...@@ -61,7 +61,7 @@ POST /projects/:id/repository/files/:file_path ...@@ -61,7 +61,7 @@ POST /projects/:id/repository/files/:file_path
``` ```
```bash ```bash
curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
``` ```
Example response: Example response:
...@@ -90,7 +90,7 @@ PUT /projects/:id/repository/files/:file_path ...@@ -90,7 +90,7 @@ PUT /projects/:id/repository/files/:file_path
``` ```
```bash ```bash
curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
``` ```
Example response: Example response:
...@@ -129,7 +129,7 @@ DELETE /projects/:id/repository/files/:file_path ...@@ -129,7 +129,7 @@ DELETE /projects/:id/repository/files/:file_path
``` ```
```bash ```bash
curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
``` ```
Example response: Example response:
......
...@@ -8,6 +8,9 @@ you to do the following: ...@@ -8,6 +8,9 @@ you to do the following:
issue index of the external tracker issue index of the external tracker
- clicking **New issue** on the project dashboard creates a new issue on the - clicking **New issue** on the project dashboard creates a new issue on the
external tracker external tracker
- you can reference these external issues inside GitLab interface
(merge requests, commits, comments) and they will be automatically converted
into links
## Configuration ## Configuration
......
...@@ -70,7 +70,27 @@ curl --location https://yarnpkg.com/install.sh | bash - ...@@ -70,7 +70,27 @@ curl --location https://yarnpkg.com/install.sh | bash -
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
### 5. Get latest code ### 5. Update Go
NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
You can check which version you are running with `go version`.
Download and install Go:
```bash
# Remove former Go installation folder
sudo rm -rf /usr/local/go
curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
rm go1.8.3.linux-amd64.tar.gz
```
### 6. Get latest code
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -97,7 +117,7 @@ cd /home/git/gitlab ...@@ -97,7 +117,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 9-2-stable-ee sudo -u git -H git checkout 9-2-stable-ee
``` ```
### 6. Update gitlab-shell ### 7. Update gitlab-shell
```bash ```bash
cd /home/git/gitlab-shell cd /home/git/gitlab-shell
...@@ -107,11 +127,10 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) ...@@ -107,11 +127,10 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile sudo -u git -H bin/compile
``` ```
### 7. Update gitlab-workhorse ### 8. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires Install and compile gitlab-workhorse. GitLab-Workhorse uses
[Go 1.8](https://golang.org/dl). Go (at least 1.5) should already be on your system from [GNU Make](https://www.gnu.org/software/make/).
GitLab 8.1 and shall be upgraded if necessary. Please note that starting in Gitlab 9.3, only Go 1.8.3 and above will be supported. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
If you are not using Linux you may have to run `gmake` instead of If you are not using Linux you may have to run `gmake` instead of
`make` below. `make` below.
...@@ -123,7 +142,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) ...@@ -123,7 +142,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
sudo -u git -H make sudo -u git -H make
``` ```
### 8. Update configuration files ### 9. Update configuration files
#### New configuration options for `gitlab.yml` #### New configuration options for `gitlab.yml`
...@@ -197,7 +216,7 @@ For Ubuntu 16.04.1 LTS: ...@@ -197,7 +216,7 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload sudo systemctl daemon-reload
``` ```
### 9. Install libs, migrations, etc. ### 10. Install libs, migrations, etc.
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -223,7 +242,7 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production ...@@ -223,7 +242,7 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
### 10. Optional: install Gitaly ### 11. Optional: install Gitaly
Gitaly is still an optional component of GitLab. If you want to save time Gitaly is still an optional component of GitLab. If you want to save time
during your 9.2 upgrade **you can skip this step**. during your 9.2 upgrade **you can skip this step**.
...@@ -240,14 +259,14 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) ...@@ -240,14 +259,14 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
sudo -u git -H make sudo -u git -H make
``` ```
### 11. Start application ### 12. Start application
```bash ```bash
sudo service gitlab start sudo service gitlab start
sudo service nginx restart sudo service nginx restart
``` ```
### 12. Check application status ### 13. Check application status
Check if GitLab and its environment are configured correctly: Check if GitLab and its environment are configured correctly:
......
...@@ -72,8 +72,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/ ...@@ -72,8 +72,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/
### 5. Update Go ### 5. Update Go
NOTE: GitLab 9.3 and higher only supports Go 1.8.3 and dropped support for Go 1.5.x through 1.7.x. Be NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
sure to upgrade your installation if necessary 1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
You can check which version you are running with `go version`. You can check which version you are running with `go version`.
...@@ -129,9 +129,8 @@ sudo -u git -H bin/compile ...@@ -129,9 +129,8 @@ sudo -u git -H bin/compile
### 8. Update gitlab-workhorse ### 8. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires Install and compile gitlab-workhorse. GitLab-Workhorse uses
[Go 1.5](https://golang.org/dl) which should already be on your system from [GNU Make](https://www.gnu.org/software/make/).
GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
If you are not using Linux you may have to run `gmake` instead of If you are not using Linux you may have to run `gmake` instead of
`make` below. `make` below.
......
...@@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla: ...@@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla:
- the **Issues** link on the GitLab project pages takes you to the appropriate - the **Issues** link on the GitLab project pages takes you to the appropriate
Bugzilla product page Bugzilla product page
- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue - clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
## Referencing issues in Bugzilla
Issues in Bugzilla can be referenced in two alternative ways:
1. `#<ID>` where `<ID>` is a number (example `#143`)
2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.
...@@ -21,3 +21,14 @@ Once you have configured and enabled Redmine: ...@@ -21,3 +21,14 @@ Once you have configured and enabled Redmine:
As an example, below is a configuration for a project named gitlab-ci. As an example, below is a configuration for a project named gitlab-ci.
![Redmine configuration](img/redmine_configuration.png) ![Redmine configuration](img/redmine_configuration.png)
## Referencing issues in Redmine
Issues in Redmine can be referenced in two alternative ways:
1. `#<ID>` where `<ID>` is a number (example `#143`)
2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.
...@@ -310,7 +310,7 @@ If there are no merge conflicts and the feature branches are short lived the ris ...@@ -310,7 +310,7 @@ If there are no merge conflicts and the feature branches are short lived the ris
If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests. If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests.
If you have long lived feature branches that last for more than a few days you should make your issues smaller. If you have long lived feature branches that last for more than a few days you should make your issues smaller.
## Working wih feature branches ## Working with feature branches
![Shell output showing git pull output](git_pull.png) ![Shell output showing git pull output](git_pull.png)
......
@dashboard
Feature: New Project
Background:
Given I sign in as a user
And I own project "Shop"
And I visit dashboard page
And I click "New project" link
@javascript
Scenario: I should see New Projects page
Then I see "New Project" page
Then I see all possible import options
@javascript
Scenario: I should see instructions on how to import from Git URL
Given I see "New Project" page
When I click on "Repo by URL"
Then I see instructions on how to import from Git URL
@javascript
Scenario: I should see instructions on how to import from GitHub
Given I see "New Project" page
When I click on "Import project from GitHub"
Then I am redirected to the GitHub import page
@javascript
Scenario: I should see Google Code import page
Given I see "New Project" page
When I click on "Google Code"
Then I redirected to Google Code import page
class Spinach::Features::NewProject < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
step 'I click "New project" link' do
page.within '#content-body' do
click_link "New project"
end
end
step 'I click "New project" in top right menu' do
page.within '.header-content' do
click_link "New project"
end
end
step 'I see "New Project" page' do
expect(page).to have_content('Project path')
expect(page).to have_content('Project name')
end
step 'I see all possible import options' do
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code')
expect(page).to have_button('Repo by URL')
expect(page).to have_link('GitLab export')
end
step 'I click on "Import project from GitHub"' do
first('.import_github').click
end
step 'I am redirected to the GitHub import page' do
expect(page).to have_content('Import Projects from GitHub')
expect(current_path).to eq new_import_github_path
end
step 'I click on "Repo by URL"' do
first('.import_git').click
end
step 'I see instructions on how to import from Git URL' do
git_import_instructions = first('.js-toggle-content')
expect(git_import_instructions).to be_visible
expect(git_import_instructions).to have_content "Git repository URL"
end
step 'I click on "Google Code"' do
first('.import_google_code').click
end
step 'I redirected to Google Code import page' do
expect(page).to have_content('Import projects from Google Code')
expect(current_path).to eq new_import_google_code_path
end
end
class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
step 'I starred project "Community"' do
current_user.toggle_star(Project.find_by(name: 'Community'))
end
step 'I should not see project "Shop"' do
page.within '.projects-list' do
expect(page).not_to have_content('Shop')
end
end
end
...@@ -216,12 +216,7 @@ module Banzai ...@@ -216,12 +216,7 @@ module Banzai
@references_per_project ||= begin @references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new } refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
if uses_reference_pattern?
Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
else
object_class.link_reference_pattern
end
nodes.each do |node| nodes.each do |node|
node.to_html.scan(regex) do node.to_html.scan(regex) do
...@@ -323,14 +318,6 @@ module Banzai ...@@ -323,14 +318,6 @@ module Banzai
value value
end end
end end
# There might be special cases like filters
# that should ignore reference pattern
# eg: IssueReferenceFilter when using a external issues tracker
# In those cases this method should be overridden on the filter subclass
def uses_reference_pattern?
true
end
end end
end end
end end
...@@ -3,6 +3,8 @@ module Banzai ...@@ -3,6 +3,8 @@ module Banzai
# HTML filter that replaces external issue tracker references with links. # HTML filter that replaces external issue tracker references with links.
# References are ignored if the project doesn't use an external issue # References are ignored if the project doesn't use an external issue
# tracker. # tracker.
#
# This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue self.reference_type = :external_issue
...@@ -87,7 +89,7 @@ module Banzai ...@@ -87,7 +89,7 @@ module Banzai
end end
def issue_reference_pattern def issue_reference_pattern
external_issues_cached(:issue_reference_pattern) external_issues_cached(:external_issue_reference_pattern)
end end
private private
......
...@@ -15,10 +15,6 @@ module Banzai ...@@ -15,10 +15,6 @@ module Banzai
Issue Issue
end end
def uses_reference_pattern?
context[:project].default_issues_tracker?
end
def find_object(project, iid) def find_object(project, iid)
issues_per_project[project][iid] issues_per_project[project][iid]
end end
...@@ -38,13 +34,7 @@ module Banzai ...@@ -38,13 +34,7 @@ module Banzai
projects_per_reference.each do |path, project| projects_per_reference.each do |path, project|
issue_ids = references_per_project[path] issue_ids = references_per_project[path]
issues = project.issues.where(iid: issue_ids.to_a)
issues =
if project.default_issues_tracker?
project.issues.where(iid: issue_ids.to_a)
else
issue_ids.map { |id| ExternalIssue.new(id, project) }
end
issues.each do |issue| issues.each do |issue|
hash[project][issue.iid.to_i] = issue hash[project][issue.iid.to_i] = issue
...@@ -55,26 +45,6 @@ module Banzai ...@@ -55,26 +45,6 @@ module Banzai
end end
end end
def object_link_title(object)
if object.is_a?(ExternalIssue)
"Issue in #{object.project.external_issue_tracker.title}"
else
super
end
end
def data_attributes_for(text, project, object, link: false)
if object.is_a?(ExternalIssue)
data_attribute(
project: project.id,
external_issue: object.id,
reference_type: ExternalIssueReferenceFilter.reference_type
)
else
super
end
end
def projects_relation_for_paths(paths) def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service) super(paths).includes(:gitlab_issue_tracker_service)
end end
......
...@@ -4,9 +4,6 @@ module Banzai ...@@ -4,9 +4,6 @@ module Banzai
self.reference_type = :issue self.reference_type = :issue
def nodes_visible_to_user(user, nodes) def nodes_visible_to_user(user, nodes)
# It is not possible to check access rights for external issue trackers
return nodes if project && project.external_issue_tracker
issues = issues_for_nodes(nodes) issues = issues_for_nodes(nodes)
readable_issues = Ability readable_issues = Ability
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true validates :name, type: String, presence: true
validates :entrypoint, type: String, allow_nil: true validates :entrypoint, array_of_strings: true, allow_nil: true
end end
def hash? def hash?
......
...@@ -15,8 +15,8 @@ module Gitlab ...@@ -15,8 +15,8 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true validates :name, type: String, presence: true
validates :entrypoint, type: String, allow_nil: true validates :entrypoint, array_of_strings: true, allow_nil: true
validates :command, type: String, allow_nil: true validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true validates :alias, type: String, allow_nil: true
end end
......
...@@ -2,11 +2,14 @@ ...@@ -2,11 +2,14 @@
module Gitlab module Gitlab
module GonHelper module GonHelper
include WebpackHelper
def add_gon_variables def add_gon_variables
gon.api_version = 'v4' gon.api_version = 'v4'
gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size gon.max_file_size = current_application_settings.max_attachment_size
gon.asset_host = ActionController::Base.asset_host gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts') gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
......
...@@ -23,6 +23,21 @@ describe Groups::MilestonesController do ...@@ -23,6 +23,21 @@ describe Groups::MilestonesController do
project.team << [user, :master] project.team << [user, :master]
end end
describe "#index" do
it 'shows group milestones page' do
get :index, group_id: group.to_param
expect(response).to have_http_status(200)
end
it 'shows group milestones JSON' do
get :index, group_id: group.to_param, format: :json
expect(response).to have_http_status(200)
expect(response.content_type).to eq 'application/json'
end
end
it_behaves_like 'milestone tabs' it_behaves_like 'milestone tabs'
describe "#create" do describe "#create" do
......
...@@ -241,7 +241,7 @@ FactoryGirl.define do ...@@ -241,7 +241,7 @@ FactoryGirl.define do
active: true, active: true,
properties: { properties: {
'project_url' => 'http://redmine/projects/project_name_in_redmine', 'project_url' => 'http://redmine/projects/project_name_in_redmine',
'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id", 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id',
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
} }
) )
......
...@@ -80,6 +80,22 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -80,6 +80,22 @@ describe 'Issue Boards', feature: true, js: true do
end end
end end
it 'does not show remove button for backlog or closed issues' do
create(:issue, project: project)
create(:issue, :closed, project: project)
visit namespace_project_board_path(project.namespace, project, board)
wait_for_requests
click_card(find('.board:nth-child(1)').first('.card'))
expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
click_card(find('.board:nth-child(3)').first('.card'))
expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
end
context 'assignee' do context 'assignee' do
it 'updates the issues assignee' do it 'updates the issues assignee' do
click_card(card) click_card(card)
......
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Dashboard Projects', feature: true do feature 'Dashboard Projects' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, name: "awesome stuff") } let(:project) { create(:project, name: 'awesome stuff') }
let(:project2) { create(:project, :public, name: 'Community project') } let(:project2) { create(:project, :public, name: 'Community project') }
before do before do
...@@ -15,6 +15,14 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -15,6 +15,14 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff') expect(page).to have_content('awesome stuff')
end end
it 'shows "New project" button' do
visit dashboard_projects_path
page.within '#content-body' do
expect(page).to have_link('New project')
end
end
context 'when last_repository_updated_at, last_activity_at and update_at are present' do context 'when last_repository_updated_at, last_activity_at and update_at are present' do
it 'shows the last_repository_updated_at attribute as the update date' do it 'shows the last_repository_updated_at attribute as the update date' do
project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago) project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
...@@ -47,8 +55,8 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -47,8 +55,8 @@ RSpec.describe 'Dashboard Projects', feature: true do
end end
end end
describe "with a pipeline", redis: true do describe 'with a pipeline', redis: true do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do before do
# Since the cache isn't updated when a new pipeline is created # Since the cache isn't updated when a new pipeline is created
......
require 'rails_helper' require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do feature 'Multiple issue updating from issues#index', :js do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
before do before do
project.team << [user, :master] project.team << [user, :master]
gitlab_sign_in(user) sign_in(user)
end end
context 'status', js: true do context 'status' do
it 'sets to closed' do it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
...@@ -37,7 +37,7 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -37,7 +37,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end end
end end
context 'assignee', js: true do context 'assignee' do
it 'updates to current user' do it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
...@@ -67,8 +67,8 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -67,8 +67,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
end end
end end
context 'milestone', js: true do context 'milestone' do
let(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
it 'updates milestone' do it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
......
require "spec_helper" require 'spec_helper'
feature "New project", feature: true do feature 'New project' do
let(:user) { create(:admin) } let(:user) { create(:admin) }
before do before do
gitlab_sign_in(user) sign_in(user)
end end
context "Visibility level selector" do it 'shows "New project" page' do
visit new_project_path
expect(page).to have_content('Project path')
expect(page).to have_content('Project name')
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code')
expect(page).to have_button('Repo by URL')
expect(page).to have_link('GitLab export')
end
context 'Visibility level selector' do
Gitlab::VisibilityLevel.options.each do |key, level| Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level) stub_application_setting(default_project_visibility: level)
...@@ -28,20 +42,20 @@ feature "New project", feature: true do ...@@ -28,20 +42,20 @@ feature "New project", feature: true do
end end
end end
context "Namespace selector" do context 'Namespace selector' do
context "with user namespace" do context 'with user namespace' do
before do before do
visit new_project_path visit new_project_path
end end
it "selects the user namespace" do it 'selects the user namespace' do
namespace = find("#project_namespace_id") namespace = find('#project_namespace_id')
expect(namespace.text).to eq user.username expect(namespace.text).to eq user.username
end end
end end
context "with group namespace" do context 'with group namespace' do
let(:group) { create(:group, :private, owner: user) } let(:group) { create(:group, :private, owner: user) }
before do before do
...@@ -49,13 +63,13 @@ feature "New project", feature: true do ...@@ -49,13 +63,13 @@ feature "New project", feature: true do
visit new_project_path(namespace_id: group.id) visit new_project_path(namespace_id: group.id)
end end
it "selects the group namespace" do it 'selects the group namespace' do
namespace = find("#project_namespace_id option[selected]") namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq group.name expect(namespace.text).to eq group.name
end end
context "on validation error" do context 'on validation error' do
before do before do
fill_in('project_path', with: 'private-group-project') fill_in('project_path', with: 'private-group-project')
choose('Internal') choose('Internal')
...@@ -64,15 +78,15 @@ feature "New project", feature: true do ...@@ -64,15 +78,15 @@ feature "New project", feature: true do
expect(page).to have_css '.project-edit-errors .alert.alert-danger' expect(page).to have_css '.project-edit-errors .alert.alert-danger'
end end
it "selects the group namespace" do it 'selects the group namespace' do
namespace = find("#project_namespace_id option[selected]") namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq group.name expect(namespace.text).to eq group.name
end end
end end
end end
context "with subgroup namespace" do context 'with subgroup namespace' do
let(:group) { create(:group, :private, owner: user) } let(:group) { create(:group, :private, owner: user) }
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
...@@ -81,8 +95,8 @@ feature "New project", feature: true do ...@@ -81,8 +95,8 @@ feature "New project", feature: true do
visit new_project_path(namespace_id: subgroup.id) visit new_project_path(namespace_id: subgroup.id)
end end
it "selects the group namespace" do it 'selects the group namespace' do
namespace = find("#project_namespace_id option[selected]") namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq subgroup.full_path expect(namespace.text).to eq subgroup.full_path
end end
...@@ -94,10 +108,45 @@ feature "New project", feature: true do ...@@ -94,10 +108,45 @@ feature "New project", feature: true do
visit new_project_path visit new_project_path
end end
it 'does not autocomplete sensitive git repo URL' do context 'from git repository url' do
autocomplete = find('#project_import_url')['autocomplete'] before do
first('.import_git').click
end
it 'does not autocomplete sensitive git repo URL' do
autocomplete = find('#project_import_url')['autocomplete']
expect(autocomplete).to eq('off')
end
it 'shows import instructions' do
git_import_instructions = first('.js-toggle-content')
expect(autocomplete).to eq('off') expect(git_import_instructions).to be_visible
expect(git_import_instructions).to have_content 'Git repository URL'
end
end
context 'from GitHub' do
before do
first('.import_github').click
end
it 'shows import instructions' do
expect(page).to have_content('Import Projects from GitHub')
expect(current_path).to eq new_import_github_path
end
end
context 'from Google Code' do
before do
first('.import_google_code').click
end
it 'shows import instructions' do
expect(page).to have_content('Import projects from Google Code')
expect(current_path).to eq new_import_google_code_path
end
end end
end end
end end
require 'rails_helper' require 'rails_helper'
describe 'User can display performacne bar', :js do describe 'User can display performance bar', :js do
shared_examples 'performance bar is disabled' do shared_examples 'performance bar is disabled' do
it 'does not show the performance bar by default' do it 'does not show the performance bar by default' do
expect(page).not_to have_css('#peek') expect(page).not_to have_css('#peek')
...@@ -27,8 +27,8 @@ describe 'User can display performacne bar', :js do ...@@ -27,8 +27,8 @@ describe 'User can display performacne bar', :js do
find('body').native.send_keys('pb') find('body').native.send_keys('pb')
end end
it 'does not show the performance bar by default' do it 'shows the performance bar' do
expect(page).not_to have_css('#peek') expect(page).to have_css('#peek')
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe MilestonesHelper do describe MilestonesHelper do
describe '#milestones_filter_dropdown_path' do
let(:project) { create(:empty_project) }
let(:project2) { create(:empty_project) }
let(:group) { create(:group) }
context 'when @project present' do
it 'returns project milestones JSON URL' do
assign(:project, project)
expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project.namespace, project, :json))
end
end
context 'when @target_project present' do
it 'returns targeted project milestones JSON URL' do
assign(:target_project, project2)
expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project2.namespace, project2, :json))
end
end
context 'when @group present' do
it 'returns group milestones JSON URL' do
assign(:group, group)
expect(helper.milestones_filter_dropdown_path).to eq(group_milestones_path(group, :json))
end
end
context 'when neither of @project/@target_project/@group present' do
it 'returns dashboard milestones JSON URL' do
expect(helper.milestones_filter_dropdown_path).to eq(dashboard_milestones_path(:json))
end
end
end
describe "#milestone_date_range" do describe "#milestone_date_range" do
def result_for(*args) def result_for(*args)
milestone_date_range(build(:milestone, *args)) milestone_date_range(build(:milestone, *args))
......
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import AwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
...@@ -26,14 +26,13 @@ import '~/lib/utils/common_utils'; ...@@ -26,14 +26,13 @@ import '~/lib/utils/common_utils';
describe('AwardsHandler', function() { describe('AwardsHandler', function() {
preloadFixtures('issues/issue_with_comment.html.raw'); preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function() { beforeEach(function(done) {
loadFixtures('issues/issue_with_comment.html.raw'); loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler; loadAwardsHandler(true).then((obj) => {
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { awardsHandler = obj;
return function(button, url, emoji, cb) { spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
return cb(); done();
}; }).catch(fail);
})(this));
let isEmojiMenuBuilt = false; let isEmojiMenuBuilt = false;
openAndWaitForEmojiMenu = function() { openAndWaitForEmojiMenu = function() {
......
...@@ -42,9 +42,6 @@ describe('Issuable output', () => { ...@@ -42,9 +42,6 @@ describe('Issuable output', () => {
}).$mount(); }).$mount();
}); });
afterEach(() => {
});
it('should render a title/description/edited and update title/description/edited on update', (done) => { it('should render a title/description/edited and update title/description/edited on update', (done) => {
vm.poll.options.successCallback({ vm.poll.options.successCallback({
json() { json() {
......
...@@ -523,6 +523,51 @@ import '~/notes'; ...@@ -523,6 +523,51 @@ import '~/notes';
}); });
}); });
describe('postComment with Slash commands', () => {
const sampleComment = '/assign @root\n/award :100:';
const note = {
commands_changes: {
assignee_id: 1,
emoji_award: '100'
},
errors: {
commands_only: ['Commands applied']
},
valid: false
};
let $form;
let $notesContainer;
beforeEach(() => {
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
gl.awardsHandler = {
addAwardToEmojiBar: () => {},
scrollToAwards: () => {}
};
gl.GfmAutoComplete = {
dataSources: {
commands: '/root/test-project/autocomplete_sources/commands'
}
};
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
$form.find('textarea.js-note-text').val(sampleComment);
});
it('should remove slash command placeholder when comment with slash commands is done posting', () => {
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
deferred.resolve(note);
expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
});
});
describe('update comment with script tags', () => { describe('update comment with script tags', () => {
const sampleComment = '<script></script>'; const sampleComment = '<script></script>';
const updatedComment = '<script></script>'; const updatedComment = '<script></script>';
......
...@@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do ...@@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'queries the collection on the first call' do it 'queries the collection on the first call' do
expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original
expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original
not_cached = reference_filter.call("look for #{reference}", { project: project }) not_cached = reference_filter.call("look for #{reference}", { project: project })
expect_any_instance_of(Project).not_to receive(:default_issues_tracker?) expect_any_instance_of(Project).not_to receive(:default_issues_tracker?)
expect_any_instance_of(Project).not_to receive(:issue_reference_pattern) expect_any_instance_of(Project).not_to receive(:external_issue_reference_pattern)
cached = reference_filter.call("look for #{reference}", { project: project }) cached = reference_filter.call("look for #{reference}", { project: project })
......
...@@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { "##{issue.iid}" } let(:reference) { "##{issue.iid}" }
it 'ignores valid references when using non-default tracker' do
allow(project).to receive(:default_issues_tracker?).and_return(false)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
end
it 'links to a valid reference' do it 'links to a valid reference' do
doc = reference_filter("Fixed #{reference}") doc = reference_filter("Fixed #{reference}")
...@@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
.to eq({ project => { issue.iid => issue } }) .to eq({ project => { issue.iid => issue } })
end end
end end
context 'using an external issue tracker' do
it 'returns a Hash containing the issues per project' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
expect(project).to receive(:default_issues_tracker?).and_return(false)
expect(filter).to receive(:projects_per_reference)
.and_return({ project.path_with_namespace => project })
expect(filter).to receive(:references_per_project)
.and_return({ project.path_with_namespace => Set.new([1]) })
expect(filter.issues_per_project[project][1])
.to be_an_instance_of(ExternalIssue)
end
end
end end
describe '.references_in' do describe '.references_in' do
......
require 'rails_helper'
describe Banzai::Pipeline::GfmPipeline do
describe 'integration between parsing regular and external issue references' do
let(:project) { create(:redmine_project, :public) }
it 'allows to use shorthand external reference syntax for Redmine' do
markdown = '#12'
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
end
it 'parses cross-project references to regular issues' do
other_project = create(:empty_project, :public)
issue = create(:issue, project: other_project)
markdown = issue.to_reference(project, full: true)
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq(
Gitlab::Routing.url_helpers.namespace_project_issue_path(
other_project.namespace,
other_project,
issue
)
)
end
end
end
...@@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do ...@@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
expect(subject.nodes_visible_to_user(user, [link])).to eq([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end end
end end
context 'when the project uses an external issue tracker' do
it 'returns all nodes' do
link = double(:link)
expect(project).to receive(:external_issue_tracker).and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end end
describe '#referenced_by' do describe '#referenced_by' do
......
...@@ -598,8 +598,10 @@ module Ci ...@@ -598,8 +598,10 @@ module Ci
describe "Image and service handling" do describe "Image and service handling" do
context "when extended docker configuration is used" do context "when extended docker configuration is used" do
it "returns image and service when defined" do it "returns image and service when defined" do
config = YAML.dump({ image: { name: "ruby:2.1" }, config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: ["mysql", { name: "docker:dind", alias: "docker" }], services: ["mysql", { name: "docker:dind", alias: "docker",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { script: "rspec" } }) rspec: { script: "rspec" } })
...@@ -614,8 +616,10 @@ module Ci ...@@ -614,8 +616,10 @@ module Ci
coverage_regex: nil, coverage_regex: nil,
tag_list: [], tag_list: [],
options: { options: {
image: { name: "ruby:2.1" }, image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }] services: [{ name: "mysql" },
{ name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }]
}, },
allow_failure: false, allow_failure: false,
when: "on_success", when: "on_success",
...@@ -628,8 +632,11 @@ module Ci ...@@ -628,8 +632,11 @@ module Ci
config = YAML.dump({ image: "ruby:2.1", config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"], services: ["mysql"],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { image: { name: "ruby:2.5" }, rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } }) services: [{ name: "postgresql", alias: "db-pg",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
script: "rspec" } })
config_processor = GitlabCiYamlProcessor.new(config, path) config_processor = GitlabCiYamlProcessor.new(config, path)
...@@ -642,8 +649,10 @@ module Ci ...@@ -642,8 +649,10 @@ module Ci
coverage_regex: nil, coverage_regex: nil,
tag_list: [], tag_list: [],
options: { options: {
image: { name: "ruby:2.5" }, image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }] services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] },
{ name: "docker:dind" }]
}, },
allow_failure: false, allow_failure: false,
when: "on_success", when: "on_success",
......
...@@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end end
context 'when configuration is a hash' do context 'when configuration is a hash' do
let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } } let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run) } }
describe '#value' do describe '#value' do
it 'returns image hash' do it 'returns image hash' do
...@@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#entrypoint' do describe '#entrypoint' do
it "returns image's entrypoint" do it "returns image's entrypoint" do
expect(entry.entrypoint).to eq '/bin/sh' expect(entry.entrypoint).to eq %w(/bin/sh run)
end end
end end
end end
......
...@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration is a hash' do context 'when configuration is a hash' do
let(:config) do let(:config) do
{ name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' } { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
end end
describe '#valid?' do describe '#valid?' do
...@@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#command' do describe '#command' do
it "returns service's command" do it "returns service's command" do
expect(entry.command).to eq 'cmd' expect(entry.command).to eq %w(cmd run)
end end
end end
describe '#entrypoint' do describe '#entrypoint' do
it "returns service's entrypoint" do it "returns service's entrypoint" do
expect(entry.entrypoint).to eq '/bin/sh' expect(entry.entrypoint).to eq %w(/bin/sh run)
end end
end end
end end
......
...@@ -64,12 +64,12 @@ describe JiraService, models: true do ...@@ -64,12 +64,12 @@ describe JiraService, models: true do
end end
end end
describe '#reference_pattern' do describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern' it_behaves_like 'allows project key on reference pattern'
it 'does not allow # on the code' do it 'does not allow # on the code' do
expect(subject.reference_pattern.match('#123')).to be_nil expect(described_class.reference_pattern.match('#123')).to be_nil
expect(subject.reference_pattern.match('1#23#12')).to be_nil expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end end
end end
......
...@@ -31,11 +31,11 @@ describe RedmineService, models: true do ...@@ -31,11 +31,11 @@ describe RedmineService, models: true do
end end
end end
describe '#reference_pattern' do describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern' it_behaves_like 'allows project key on reference pattern'
it 'does allow # on the reference' do it 'does allow # on the reference' do
expect(subject.reference_pattern.match('#123')[:issue]).to eq('123') expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123')
end end
end end
end end
...@@ -436,18 +436,6 @@ describe GitPushService, services: true do ...@@ -436,18 +436,6 @@ describe GitPushService, services: true do
expect(SystemNoteService).not_to receive(:cross_reference) expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, commit_author, oldrev, newrev, ref) execute_service(project, commit_author, oldrev, newrev, ref)
end end
it "doesn't close issues when external issue tracker is in use" do
allow_any_instance_of(Project).to receive(:default_issues_tracker?)
.and_return(false)
external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
# The push still shouldn't create cross-reference notes.
expect do
execute_service(project, commit_author, oldrev, newrev, 'refs/heads/hurf')
end.not_to change { Note.where(project_id: project.id, system: true).count }
end
end end
context "to non-default branches" do context "to non-default branches" do
......
...@@ -8,15 +8,15 @@ end ...@@ -8,15 +8,15 @@ end
RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
it 'allows underscores in the project name' do it 'allows underscores in the project name' do
expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end end
it 'allows numbers in the project name' do it 'allows numbers in the project name' do
expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
end end
it 'requires the project name to begin with A-Z' do it 'requires the project name to begin with A-Z' do
expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil
expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end end
end end
...@@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0: ...@@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0:
slash "^1.0.0" slash "^1.0.0"
source-map "^0.5.0" source-map "^0.5.0"
babel-eslint@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f"
dependencies:
babel-code-frame "^6.22.0"
babel-traverse "^6.23.1"
babel-types "^6.23.0"
babylon "^6.16.1"
babel-generator@^6.18.0, babel-generator@^6.23.0: babel-generator@^6.18.0, babel-generator@^6.23.0:
version "6.23.0" version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5"
...@@ -816,10 +825,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23 ...@@ -816,10 +825,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23
lodash "^4.2.0" lodash "^4.2.0"
to-fast-properties "^1.0.1" to-fast-properties "^1.0.1"
babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: babylon@^6.11.0:
version "6.15.0" version "6.15.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1:
version "6.16.1"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
backo2@1.0.2: backo2@1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
......
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