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 @@
"gon": false,
"localStorage": false
},
"parser": "babel-eslint",
"plugins": [
"filenames",
"import",
......
......@@ -460,8 +460,6 @@ codeclimate:
services:
- docker:dind
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
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts:
......
......@@ -2,7 +2,6 @@
/* global Flash */
import Cookies from 'js-cookie';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
......@@ -24,27 +23,9 @@ const categoryLabelMap = {
flags: 'Flags',
};
function 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">
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
export default class AwardsHandler {
constructor() {
class AwardsHandler {
constructor(emoji) {
this.emoji = emoji;
this.eventListeners = [];
// If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
......@@ -78,10 +59,10 @@ export default class AwardsHandler {
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
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');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
});
}
......@@ -139,16 +120,16 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true;
// Render the first category
const categoryMap = Emoji.getEmojiCategoryMap();
const categoryMap = this.emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
// Render the frequently used
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
menuListClass: 'frequent-emojis',
});
}
......@@ -179,7 +160,7 @@ export default class AwardsHandler {
}
this.isAddingRemainingEmojiMenuCategories = true;
const categoryMap = Emoji.getEmojiCategoryMap();
const categoryMap = this.emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
......@@ -191,7 +172,7 @@ export default class AwardsHandler {
promiseChain.then(() =>
new Promise((resolve) => {
const emojisInCategory = categoryMap[categoryNameKey];
const categoryMarkup = renderCategory(
const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey],
emojisInCategory,
);
......@@ -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) {
const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element
......@@ -234,7 +234,7 @@ export default class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
......@@ -249,7 +249,7 @@ export default class AwardsHandler {
this.checkMutuality(votesBlock, emoji);
}
this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) {
......@@ -374,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
<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>
</button>
`;
......@@ -440,7 +440,7 @@ export default class AwardsHandler {
}
addEmojiToFrequentlyUsedList(emoji) {
if (Emoji.isEmojiNameValid(emoji)) {
if (this.emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
......@@ -450,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => Emoji.isEmojiNameValid(inputName),
inputName => this.emoji.isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
......@@ -493,7 +493,7 @@ export default class AwardsHandler {
}
findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
......@@ -507,3 +507,12 @@ export default class AwardsHandler {
$('.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 { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window);
......@@ -32,11 +31,19 @@ export default function installGlEmojiElement() {
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
import(/* webpackChunkName: 'emoji' */ '../emoji')
.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({
},
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
},
canRemove() {
return !this.list.preset;
},
},
watch: {
detail: {
......
......@@ -51,8 +51,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
},
template: `
<div
class="block list"
v-if="list.type !== 'closed'">
class="block list">
<button
class="btn btn-default btn-block"
type="button"
......
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
......@@ -373,7 +372,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} 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 {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => {
......@@ -428,12 +432,14 @@ GfmAutoComplete.atTypeMap = {
};
// Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = {
templateFunction(name) {
return `<li>
${name} ${glEmojiTag(name)}
</li>
`;
// glEmojiTag helper is loaded on-demand in fetchData()
if (GfmAutoComplete.glEmojiTag) {
return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
}
return `<li>${name}</li>`;
},
};
// Team Members
......
......@@ -5,6 +5,7 @@
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
......@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
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() {
IssuableBulkUpdateActions.setOriginalDropdownData();
}
......@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
this.toggleCheckboxDisplay(enable);
if (enable) {
this.initSidebar();
SidebarHeightManager.init();
}
}
......@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
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() {
const $checkedIssues = $('.selected_issue:checked');
......
......@@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
return timefor;
};
w.gl.utils.cachedTimeagoElements = [];
w.gl.utils.renderTimeago = function($els) {
if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
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();
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
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) {
......
......@@ -70,7 +70,7 @@ import './ajax_loading_spinner';
import './api';
import './aside';
import './autosave';
import AwardsHandler from './awards_handler';
import loadAwardsHandler from './awards_handler';
import './breakpoints';
import './broadcast_message';
import './build';
......@@ -366,10 +366,10 @@ $(function () {
$window.off('resize.app').on('resize.app', function () {
return fitSidebarForSize();
});
gl.awardsHandler = new AwardsHandler();
loadAwardsHandler();
new Aside();
gl.utils.initTimeagoTimeout();
gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs');
});
......@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
import './task_list';
......@@ -291,8 +292,13 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) {
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 {
if (!noteEntity.valid) {
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.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 */
import Cookies from 'js-cookie';
import SidebarHeightManager from './sidebar_height_manager';
(function() {
this.Sidebar = (function() {
......@@ -8,12 +9,6 @@ import Cookies from 'js-cookie';
this.toggleTodo = this.toggleTodo.bind(this);
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.addEventListeners();
}
......@@ -27,16 +22,14 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.addEventListeners = function() {
SidebarHeightManager.init();
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);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => slowerThrottledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
......@@ -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() {
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 @@
max-width: $limited-layout-width-sm;
margin-left: auto;
margin-right: auto;
padding-top: 64px;
padding-bottom: 64px;
}
}
......
......@@ -49,6 +49,7 @@ $new-sidebar-width: 220px;
position: fixed;
z-index: 400;
width: $new-sidebar-width;
transition: width $sidebar-transition-duration;
top: 50px;
bottom: 0;
left: 0;
......@@ -62,6 +63,8 @@ $new-sidebar-width: 220px;
}
li {
white-space: nowrap;
a {
display: block;
padding: 12px 14px;
......@@ -72,6 +75,10 @@ $new-sidebar-width: 220px;
color: $gl-text-color;
text-decoration: none;
}
@media (max-width: $screen-xs-max) {
width: 0;
}
}
.sidebar-sub-level-items {
......
......@@ -491,11 +491,12 @@ a.deploy-project-label {
.project-stats {
font-size: 0;
text-align: center;
max-width: 100%;
border-bottom: 1px solid $border-color;
.nav {
padding-top: 12px;
padding-bottom: 12px;
border-bottom: 1px solid $border-color;
}
.nav > li {
......
......@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
render json: milestones.map { |m| m.for_display.slice(:title, :name) }
end
end
end
......
......@@ -165,7 +165,6 @@ module IssuablesHelper
}
state_title = titles[state] || state.to_s.humanize
count = issuables_count_for_state(issuable_type, state)
html = content_tag(:span, state_title)
......
......@@ -74,6 +74,8 @@ module MilestonesHelper
project = @target_project || @project
if project
namespace_project_milestones_path(project.namespace, project, :json)
elsif @group
group_milestones_path(@group, :json)
else
dashboard_milestones_path(:json)
end
......
......@@ -11,20 +11,29 @@ module WebpackHelper
paths = Webpack::Rails::Manifest.asset_paths(source)
if extension
paths = paths.select { |p| p.ends_with? ".#{extension}" }
paths.select! { |p| p.ends_with? ".#{extension}" }
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
host = Rails.configuration.webpack.dev_server.host
port = Rails.configuration.webpack.dev_server.port
protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
paths.map! do |p|
"#{protocol}://#{host}:#{port}#{p}"
end
"#{protocol}://#{host}:#{port}"
else
ActionController::Base.asset_host.try(:chomp, '/')
end
end
paths
def webpack_public_path
"#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/"
end
end
......@@ -14,7 +14,7 @@ module Mentionable
end
EXTERNAL_PATTERN = begin
issue_pattern = ExternalIssue.reference_pattern
issue_pattern = IssueTrackerService.reference_pattern
link_patterns = URI.regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern)
end
......
......@@ -38,11 +38,6 @@ class ExternalIssue
@project.id
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)
id
end
......
......@@ -728,8 +728,8 @@ class Project < ActiveRecord::Base
end
end
def issue_reference_pattern
issues_tracker.reference_pattern
def external_issue_reference_pattern
external_issue_tracker.class.reference_pattern
end
def default_issues_tracker?
......
......@@ -5,7 +5,10 @@ class IssueTrackerService < Service
# Pattern used to extract links from comments
# 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+)}
end
......
......@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService
end
# {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+)}
end
......
......@@ -38,7 +38,7 @@
= Gon::Base.render_data
= webpack_bundle_tag "runtime"
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
= webpack_bundle_tag "locale"
= webpack_bundle_tag "main"
......
......@@ -23,4 +23,5 @@
= render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":list" => "list" }
":list" => "list",
"v-if" => "canRemove" }
......@@ -18,9 +18,6 @@
= 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?
%div{ class: container_class }
.top-area
......
......@@ -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
#{ _('Set up auto deploy') }
%div{ class: container_class }
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
......
- if @issues.to_a.any?
.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
= paginate @issues, theme: "gitlab"
- else
......
- if @merge_requests.to_a.any?
.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
= paginate @merge_requests, theme: "gitlab"
......
- 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_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source) },
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]),
method: :delete,
data: { confirm: remove_member_message(requester) },
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]),
method: :post,
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 = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
peek: './peek.js',
webpack_runtime: './webpack.js',
},
output: {
......@@ -197,7 +198,7 @@ var config = {
// create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({
names: ['main', 'locale', 'common', 'runtime'],
names: ['main', 'locale', 'common', 'webpack_runtime'],
}),
],
......@@ -252,7 +253,6 @@ if (IS_DEV_SERVER) {
hot: DEV_SERVER_LIVERELOAD,
inline: DEV_SERVER_LIVERELOAD
};
config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath;
config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
......
......@@ -61,7 +61,7 @@ POST /projects/:id/repository/files/:file_path
```
```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:
......@@ -90,7 +90,7 @@ PUT /projects/:id/repository/files/:file_path
```
```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:
......@@ -129,7 +129,7 @@ DELETE /projects/:id/repository/files/:file_path
```
```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:
......
......@@ -8,6 +8,9 @@ you to do the following:
issue index of the external tracker
- clicking **New issue** on the project dashboard creates a new issue on the
external tracker
- you can reference these external issues inside GitLab interface
(merge requests, commits, comments) and they will be automatically converted
into links
## Configuration
......
......@@ -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).
### 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
cd /home/git/gitlab
......@@ -97,7 +117,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 9-2-stable-ee
```
### 6. Update gitlab-shell
### 7. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
......@@ -107,11 +127,10 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile
```
### 7. Update gitlab-workhorse
### 8. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.8](https://golang.org/dl). Go (at least 1.5) should already be on your system from
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/).
Install and compile gitlab-workhorse. 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
`make` below.
......@@ -123,7 +142,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
sudo -u git -H make
```
### 8. Update configuration files
### 9. Update configuration files
#### New configuration options for `gitlab.yml`
......@@ -197,7 +216,7 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
```
### 9. Install libs, migrations, etc.
### 10. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
......@@ -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).
### 10. Optional: install Gitaly
### 11. Optional: install Gitaly
Gitaly is still an optional component of GitLab. If you want to save time
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)
sudo -u git -H make
```
### 11. Start application
### 12. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 12. Check application status
### 13. Check application status
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/
### 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
sure to upgrade your installation if necessary
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`.
......@@ -129,9 +129,8 @@ sudo -u git -H bin/compile
### 8. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
Install and compile gitlab-workhorse. 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
`make` below.
......
......@@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla:
- the **Issues** link on the GitLab project pages takes you to the appropriate
Bugzilla product page
- 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:
As an example, below is a configuration for a project named gitlab-ci.
![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
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.
## Working wih feature branches
## Working with feature branches
![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
@references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
regex =
if uses_reference_pattern?
Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
else
object_class.link_reference_pattern
end
regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
nodes.each do |node|
node.to_html.scan(regex) do
......@@ -323,14 +318,6 @@ module Banzai
value
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
......@@ -3,6 +3,8 @@ module Banzai
# HTML filter that replaces external issue tracker references with links.
# References are ignored if the project doesn't use an external issue
# tracker.
#
# This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
......@@ -87,7 +89,7 @@ module Banzai
end
def issue_reference_pattern
external_issues_cached(:issue_reference_pattern)
external_issues_cached(:external_issue_reference_pattern)
end
private
......
......@@ -15,10 +15,6 @@ module Banzai
Issue
end
def uses_reference_pattern?
context[:project].default_issues_tracker?
end
def find_object(project, iid)
issues_per_project[project][iid]
end
......@@ -38,13 +34,7 @@ module Banzai
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
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 = project.issues.where(iid: issue_ids.to_a)
issues.each do |issue|
hash[project][issue.iid.to_i] = issue
......@@ -55,26 +45,6 @@ module Banzai
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)
super(paths).includes(:gitlab_issue_tracker_service)
end
......
......@@ -4,9 +4,6 @@ module Banzai
self.reference_type = :issue
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)
readable_issues = Ability
......
......@@ -15,7 +15,7 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true
validates :entrypoint, type: String, allow_nil: true
validates :entrypoint, array_of_strings: true, allow_nil: true
end
def hash?
......
......@@ -15,8 +15,8 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true
validates :entrypoint, type: String, allow_nil: true
validates :command, type: String, allow_nil: true
validates :entrypoint, array_of_strings: true, allow_nil: true
validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true
end
......
......@@ -2,11 +2,14 @@
module Gitlab
module GonHelper
include WebpackHelper
def add_gon_variables
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.max_file_size = current_application_settings.max_attachment_size
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.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
......
......@@ -23,6 +23,21 @@ describe Groups::MilestonesController do
project.team << [user, :master]
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'
describe "#create" do
......
......@@ -241,7 +241,7 @@ FactoryGirl.define do
active: true,
properties: {
'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'
}
)
......
......@@ -80,6 +80,22 @@ describe 'Issue Boards', feature: true, js: true do
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
it 'updates the issues assignee' do
click_card(card)
......
require 'spec_helper'
RSpec.describe 'Dashboard Projects', feature: true do
feature 'Dashboard Projects' do
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') }
before do
......@@ -15,6 +15,14 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff')
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
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)
......@@ -47,8 +55,8 @@ RSpec.describe 'Dashboard Projects', feature: true do
end
end
describe "with a pipeline", redis: true do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
describe 'with a pipeline', redis: true do
let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do
# Since the cache isn't updated when a new pipeline is created
......
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!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
project.team << [user, :master]
gitlab_sign_in(user)
sign_in(user)
end
context 'status', js: true do
context 'status' do
it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project)
......@@ -37,7 +37,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
end
context 'assignee', js: true do
context 'assignee' do
it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project)
......@@ -67,8 +67,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
end
context 'milestone', js: true do
let(:milestone) { create(:milestone, project: project) }
context 'milestone' do
let!(:milestone) { create(:milestone, project: project) }
it 'updates milestone' do
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) }
before do
gitlab_sign_in(user)
sign_in(user)
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|
it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level)
......@@ -28,20 +42,20 @@ feature "New project", feature: true do
end
end
context "Namespace selector" do
context "with user namespace" do
context 'Namespace selector' do
context 'with user namespace' do
before do
visit new_project_path
end
it "selects the user namespace" do
namespace = find("#project_namespace_id")
it 'selects the user namespace' do
namespace = find('#project_namespace_id')
expect(namespace.text).to eq user.username
end
end
context "with group namespace" do
context 'with group namespace' do
let(:group) { create(:group, :private, owner: user) }
before do
......@@ -49,13 +63,13 @@ feature "New project", feature: true do
visit new_project_path(namespace_id: group.id)
end
it "selects the group namespace" do
namespace = find("#project_namespace_id option[selected]")
it 'selects the group namespace' do
namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq group.name
end
context "on validation error" do
context 'on validation error' do
before do
fill_in('project_path', with: 'private-group-project')
choose('Internal')
......@@ -64,15 +78,15 @@ feature "New project", feature: true do
expect(page).to have_css '.project-edit-errors .alert.alert-danger'
end
it "selects the group namespace" do
namespace = find("#project_namespace_id option[selected]")
it 'selects the group namespace' do
namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq group.name
end
end
end
context "with subgroup namespace" do
context 'with subgroup namespace' do
let(:group) { create(:group, :private, owner: user) }
let(:subgroup) { create(:group, parent: group) }
......@@ -81,8 +95,8 @@ feature "New project", feature: true do
visit new_project_path(namespace_id: subgroup.id)
end
it "selects the group namespace" do
namespace = find("#project_namespace_id option[selected]")
it 'selects the group namespace' do
namespace = find('#project_namespace_id option[selected]')
expect(namespace.text).to eq subgroup.full_path
end
......@@ -94,10 +108,45 @@ feature "New project", feature: true do
visit new_project_path
end
it 'does not autocomplete sensitive git repo URL' do
autocomplete = find('#project_import_url')['autocomplete']
context 'from git repository url' do
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
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
it 'does not show the performance bar by default' do
expect(page).not_to have_css('#peek')
......@@ -27,8 +27,8 @@ describe 'User can display performacne bar', :js do
find('body').native.send_keys('pb')
end
it 'does not show the performance bar by default' do
expect(page).not_to have_css('#peek')
it 'shows the performance bar' do
expect(page).to have_css('#peek')
end
end
end
......
require 'spec_helper'
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
def result_for(*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 */
import Cookies from 'js-cookie';
import AwardsHandler from '~/awards_handler';
import loadAwardsHandler from '~/awards_handler';
import '~/lib/utils/common_utils';
......@@ -26,14 +26,13 @@ import '~/lib/utils/common_utils';
describe('AwardsHandler', function() {
preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function() {
beforeEach(function(done) {
loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(button, url, emoji, cb) {
return cb();
};
})(this));
loadAwardsHandler(true).then((obj) => {
awardsHandler = obj;
spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
done();
}).catch(fail);
let isEmojiMenuBuilt = false;
openAndWaitForEmojiMenu = function() {
......
......@@ -42,9 +42,6 @@ describe('Issuable output', () => {
}).$mount();
});
afterEach(() => {
});
it('should render a title/description/edited and update title/description/edited on update', (done) => {
vm.poll.options.successCallback({
json() {
......
......@@ -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', () => {
const sampleComment = '<script></script>';
const updatedComment = '<script></script>';
......
......@@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true 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(: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 })
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 })
......
......@@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
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
doc = reference_filter("Fixed #{reference}")
......@@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
.to eq({ project => { issue.iid => issue } })
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
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
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
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
describe '#referenced_by' do
......
......@@ -598,8 +598,10 @@ module Ci
describe "Image and service handling" do
context "when extended docker configuration is used" do
it "returns image and service when defined" do
config = YAML.dump({ image: { name: "ruby:2.1" },
services: ["mysql", { name: "docker:dind", alias: "docker" }],
config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: ["mysql", { name: "docker:dind", alias: "docker",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }],
before_script: ["pwd"],
rspec: { script: "rspec" } })
......@@ -614,8 +616,10 @@ module Ci
coverage_regex: nil,
tag_list: [],
options: {
image: { name: "ruby:2.1" },
services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }]
image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "mysql" },
{ name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }]
},
allow_failure: false,
when: "on_success",
......@@ -628,8 +632,11 @@ module Ci
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: { image: { name: "ruby:2.5" },
services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } })
rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
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)
......@@ -642,8 +649,10 @@ module Ci
coverage_regex: nil,
tag_list: [],
options: {
image: { name: "ruby:2.5" },
services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }]
image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] },
{ name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
......
......@@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do
end
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
it 'returns image hash' do
......@@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#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
......
......@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration is a hash' 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
describe '#valid?' do
......@@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#command' do
it "returns service's command" do
expect(entry.command).to eq 'cmd'
expect(entry.command).to eq %w(cmd run)
end
end
describe '#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
......
......@@ -64,12 +64,12 @@ describe JiraService, models: true do
end
end
describe '#reference_pattern' do
describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
it 'does not allow # on the code' do
expect(subject.reference_pattern.match('#123')).to be_nil
expect(subject.reference_pattern.match('1#23#12')).to be_nil
expect(described_class.reference_pattern.match('#123')).to be_nil
expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end
end
......
......@@ -31,11 +31,11 @@ describe RedmineService, models: true do
end
end
describe '#reference_pattern' do
describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
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
......@@ -436,18 +436,6 @@ describe GitPushService, services: true do
expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, commit_author, oldrev, newrev, ref)
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
context "to non-default branches" do
......
......@@ -8,15 +8,15 @@ end
RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
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
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
it 'requires the project name to begin with A-Z' do
expect(subject.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('3EXT_EXT-1234')).to eq nil
expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end
end
......@@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0:
slash "^1.0.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:
version "6.23.0"
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
lodash "^4.2.0"
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"
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:
version "1.0.2"
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