Commit 9e7a691b authored by James Lopez's avatar James Lopez

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into dev-master

# Conflicts:
#	CHANGELOG.md
parents e832c9c7 25bdf50b
...@@ -426,7 +426,7 @@ merge request: ...@@ -426,7 +426,7 @@ merge request:
1. [Ruby](https://github.com/bbatsov/ruby-style-guide). 1. [Ruby](https://github.com/bbatsov/ruby-style-guide).
Important sections include [Source Code Layout][rss-source] and Important sections include [Source Code Layout][rss-source] and
[Naming][rss-naming]. Use: [Naming][rss-naming]. Use:
- multi-line method chaining style **Option B**: dot `.` on previous line - multi-line method chaining style **Option A**: dot `.` on the second line
- string literal quoting style **Option A**: single quoted by default - string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide) 1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide] 1. [Newlines styleguide][newlines-styleguide]
......
...@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0' ...@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.2' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-cas3', '~> 1.1.2'
......
...@@ -352,7 +352,7 @@ GEM ...@@ -352,7 +352,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashie (3.5.1) hashie (3.5.5)
health_check (2.2.1) health_check (2.2.1)
rails (>= 4.0) rails (>= 4.0)
hipchat (1.5.2) hipchat (1.5.2)
...@@ -465,7 +465,7 @@ GEM ...@@ -465,7 +465,7 @@ GEM
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.4)
omniauth (1.3.2) omniauth (1.4.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
...@@ -951,7 +951,7 @@ DEPENDENCIES ...@@ -951,7 +951,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0) oauth2 (~> 1.2.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.2) omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.0) omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
......
...@@ -7,10 +7,6 @@ ...@@ -7,10 +7,6 @@
/* global Aside */ /* global Aside */
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete');
require('jquery-ui/ui/draggable');
require('jquery-ui/ui/effect-highlight');
require('jquery-ui/ui/sortable');
require('jquery-ujs'); require('jquery-ujs');
require('vendor/jquery.endless-scroll'); require('vendor/jquery.endless-scroll');
require('vendor/jquery.highlight'); require('vendor/jquery.highlight');
...@@ -108,6 +104,7 @@ require('./droplab/droplab_filter'); ...@@ -108,6 +104,7 @@ require('./droplab/droplab_filter');
require('./abuse_reports'); require('./abuse_reports');
require('./activities'); require('./activities');
require('./admin'); require('./admin');
require('./ajax_loading_spinner');
require('./api'); require('./api');
require('./aside'); require('./aside');
require('./autosave'); require('./autosave');
......
/* global Vue */
require('./issue_card_inner');
const Store = gl.issueBoards.BoardsStore;
module.exports = {
name: 'BoardsIssueCard',
template: `
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath" />
</li>
`,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
index: Number,
rootPath: String,
},
data() {
return {
showDetail: false,
detailIssue: Store.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase();
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
Store.detail.list = this.list;
}
}
},
},
};
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
/* global Vue */
require('./issue_card_inner');
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
index: Number,
rootPath: String,
},
data () {
return {
showDetail: false,
detailIssue: Store.detail
};
},
computed: {
issueDetailVisible () {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
}
},
methods: {
mouseDown () {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
Store.detail.list = this.list;
}
}
}
}
});
})();
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global Vue */ /* global Vue */
/* global Sortable */ /* global Sortable */
require('./board_card'); const boardCard = require('./board_card');
require('./board_new_issue'); require('./board_new_issue');
(() => { (() => {
...@@ -14,7 +14,7 @@ require('./board_new_issue'); ...@@ -14,7 +14,7 @@ require('./board_new_issue');
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template', template: '#js-board-list-template',
components: { components: {
'board-card': gl.issueBoards.BoardCard, boardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue 'board-new-issue': gl.issueBoards.BoardNewIssue
}, },
props: { props: {
......
...@@ -123,14 +123,18 @@ class List { ...@@ -123,14 +123,18 @@ class List {
if (listFrom) { if (listFrom) {
this.issuesSize += 1; this.issuesSize += 1;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id) this.updateIssueLabel(issue, listFrom);
.then(() => {
listFrom.getIssues(false);
});
} }
} }
} }
updateIssueLabel(issue, listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
}
findIssue (id) { findIssue (id) {
return this.issues.filter(issue => issue.id === id)[0]; return this.issues.filter(issue => issue.id === id)[0];
} }
......
...@@ -97,9 +97,12 @@ ...@@ -97,9 +97,12 @@
const issueLists = issue.getLists(); const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label); const listLabels = issueLists.map(listIssue => listIssue.label);
// Add to new lists issues if it doesn't already exist
if (!issueTo) { if (!issueTo) {
// Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex); listTo.addIssue(issue, listFrom, newIndex);
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
} }
if (listTo.type === 'done') { if (listTo.type === 'done') {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
var DOWN_BUILD_TRACE = '#down-build-trace'; var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() { this.Build = (function() {
Build.interval = null; Build.timeout = null;
Build.state = null; Build.state = null;
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
this.$scrollBottomBtn = $('#scroll-bottom'); this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
clearInterval(Build.interval); clearTimeout(Build.timeout);
// Init breakpoint checker // Init breakpoint checker
this.bp = Breakpoints.get(); this.bp = Breakpoints.get();
...@@ -52,17 +52,7 @@ ...@@ -52,17 +52,7 @@
this.getInitialBuildTrace(); this.getInitialBuildTrace();
this.initScrollButtonAffix(); this.initScrollButtonAffix();
} }
if (this.buildStatus === "running" || this.buildStatus === "pending") { this.invokeBuildTrace();
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
return function() {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
})(this), 4000);
}
} }
Build.prototype.initSidebar = function() { Build.prototype.initSidebar = function() {
...@@ -75,6 +65,22 @@ ...@@ -75,6 +65,22 @@
return window.location.href.split("#")[0]; return window.location.href.split("#")[0];
}; };
Build.prototype.invokeBuildTrace = function() {
var continueRefreshStatuses = ['running', 'pending'];
// Continue to update build trace when build is running or pending
if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
Build.timeout = setTimeout((function(_this) {
return function() {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
})(this), 4000);
}
};
Build.prototype.getInitialBuildTrace = function() { Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
...@@ -86,7 +92,7 @@ ...@@ -86,7 +92,7 @@
if (window.location.hash === DOWN_BUILD_TRACE) { if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height()); $("html,body").scrollTop(this.$buildTrace.height());
} }
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
return this.initScrollMonitor(); return this.initScrollMonitor();
} }
...@@ -105,6 +111,7 @@ ...@@ -105,6 +111,7 @@
if (log.state) { if (log.state) {
_this.state = log.state; _this.state = log.state;
} }
_this.invokeBuildTrace();
if (log.status === "running") { if (log.status === "running") {
if (log.append) { if (log.append) {
$('.js-build-output').append(log.html); $('.js-build-output').append(log.html);
......
...@@ -52,6 +52,30 @@ ...@@ -52,6 +52,30 @@
return this.views[viewMode].call(this); return this.views[viewMode].call(this);
}; };
ImageFile.prototype.initDraggable = function($el, padding, callback) {
var dragging = false;
var $body = $('body');
var $offsetEl = $el.parent();
$el.off('mousedown').on('mousedown', function() {
dragging = true;
$body.css('user-select', 'none');
});
$body.off('mouseup').off('mousemove').on('mouseup', function() {
dragging = false;
$body.css('user-select', '');
})
.on('mousemove', function(e) {
var left;
if (!dragging) return;
left = e.pageX - ($offsetEl.offset().left + padding);
callback(e, left);
});
};
prepareFrames = function(view) { prepareFrames = function(view) {
var maxHeight, maxWidth; var maxHeight, maxWidth;
maxWidth = 0; maxWidth = 0;
...@@ -96,26 +120,30 @@ ...@@ -96,26 +120,30 @@
maxHeight = 0; maxHeight = 0;
return $('.swipe.view', this.file).each((function(_this) { return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var ref; var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$('.swipe-frame', view).css({ $swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
$swipeFrame.css({
width: maxWidth + 16, width: maxWidth + 16,
height: maxHeight + 28 height: maxHeight + 28
}); });
$('.swipe-wrap', view).css({ $swipeWrap.css({
width: maxWidth + 1, width: maxWidth + 1,
height: maxHeight + 2 height: maxHeight + 2
}); });
return $('.swipe-bar', view).css({ $swipeBar.css({
left: 0 left: 0
}).draggable({ });
axis: 'x',
containment: 'parent', wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
drag: function(event) {
return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
}, if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
stop: function(event) { $swipeWrap.width((maxWidth + 1) - left);
return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); $swipeBar.css('left', left);
} }
}); });
}; };
...@@ -128,9 +156,14 @@ ...@@ -128,9 +156,14 @@
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each((function(_this) { return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var ref; var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$('.onion-skin-frame', view).css({ $frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
$dragger = $('.dragger', $track);
$frame.css({
width: maxWidth + 16, width: maxWidth + 16,
height: maxHeight + 28 height: maxHeight + 28
}); });
...@@ -138,16 +171,18 @@ ...@@ -138,16 +171,18 @@
width: maxWidth + 1, width: maxWidth + 1,
height: maxHeight + 2 height: maxHeight + 2
}); });
return $('.dragger', view).css({ $dragger.css({
left: dragTrackWidth left: dragTrackWidth
}).draggable({ });
axis: 'x',
containment: 'parent', framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
drag: function(event) {
return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); _this.initDraggable($dragger, framePadding, function(e, left) {
}, var opacity = left / dragTrackWidth;
stop: function(event) {
return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); if (opacity >= 0 && opacity <= 1) {
$dragger.css('left', left);
$frameAdded.css('opacity', opacity);
} }
}); });
}; };
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
/* global AdminEmailSelect */ /* global AdminEmailSelect */
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -287,6 +288,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -287,6 +288,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'ci:lints:show': case 'ci:lints:show':
new gl.CILintEditor(); new gl.CILintEditor();
break; break;
case 'users:show':
new UserCallout();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'sessions': case 'sessions':
...@@ -326,6 +330,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -326,6 +330,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard': case 'dashboard':
case 'root': case 'root':
shortcut_handler = new ShortcutsDashboardNavigation(); shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout();
break; break;
case 'profiles': case 'profiles':
new NotificationsForm(); new NotificationsForm();
......
...@@ -523,32 +523,30 @@ module.exports = Vue.component('environment-item', { ...@@ -523,32 +523,30 @@ module.exports = Vue.component('environment-item', {
</span> </span>
</td> </td>
<td class="hidden-xs"> <td class="hidden-xs environments-actions">
<div v-if="!model.isFolder"> <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
<div class="btn-group" role="group"> <actions-component v-if="hasManualActions && canCreateDeployment"
<actions-component v-if="hasManualActions && canCreateDeployment" :play-icon-svg="playIconSvg"
:play-icon-svg="playIconSvg" :actions="manualActions">
:actions="manualActions"> </actions-component>
</actions-component>
<external-url-component v-if="externalURL && canReadEnvironment"
<external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL">
:external-url="externalURL"> </external-url-component>
</external-url-component>
<stop-component v-if="hasStopAction && canCreateDeployment"
<stop-component v-if="hasStopAction && canCreateDeployment" :stop-url="model.stop_path">
:stop-url="model.stop_path"> </stop-component>
</stop-component>
<terminal-button-component v-if="model && model.terminal_path"
<terminal-button-component v-if="model && model.terminal_path" :terminal-icon-svg="terminalIconSvg"
:terminal-icon-svg="terminalIconSvg" :terminal-path="model.terminal_path">
:terminal-path="model.terminal_path"> </terminal-button-component>
</terminal-button-component>
<rollback-component v-if="canRetry && canCreateDeployment"
<rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment"
:is-last-deployment="isLastDeployment" :retry-url="retryUrl">
:retry-url="retryUrl"> </rollback-component>
</rollback-component>
</div>
</div> </div>
</td> </td>
</tr> </tr>
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */ /* global FilesCommentButton */
/* global notes */
(function() { (function() {
let $commentButtonTemplate;
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.FilesCommentButton = (function() { this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
COMMENT_BUTTON_CLASS = '.add-diff-note'; COMMENT_BUTTON_CLASS = '.add-diff-note';
COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
LINE_HOLDER_CLASS = '.line_holder'; LINE_HOLDER_CLASS = '.line_holder';
LINE_NUMBER_CLASS = 'diff-line-num'; LINE_NUMBER_CLASS = 'diff-line-num';
...@@ -27,26 +27,29 @@ ...@@ -27,26 +27,29 @@
TEXT_FILE_SELECTOR = '.text-file'; TEXT_FILE_SELECTOR = '.text-file';
DEBOUNCE_TIMEOUT_DURATION = 100;
function FilesCommentButton(filesContainerElement) { function FilesCommentButton(filesContainerElement) {
var debounce;
this.filesContainerElement = filesContainerElement;
this.destroy = bind(this.destroy, this);
this.render = bind(this.render, this); this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val(); this.hideButton = bind(this.hideButton, this);
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); this.isParallelView = notes.isParallelView();
$(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
} }
FilesCommentButton.prototype.render = function(e) { FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement; var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget); $currentTarget = $(e.currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement)) return;
lineContentElement = this.getLineContent($currentTarget); lineContentElement = this.getLineContent($currentTarget);
if (!this.validateLineContent(lineContentElement)) return; buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
}
textFileElement = this.getTextFileElement($currentTarget); textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({ buttonParentElement.append(this.buildButton({
...@@ -61,19 +64,16 @@ ...@@ -61,19 +64,16 @@
})); }));
}; };
FilesCommentButton.prototype.destroy = function(e) { FilesCommentButton.prototype.hideButton = function(e) {
if (this.isMovingToSameType(e)) { var $currentTarget = $(e.currentTarget);
return; var buttonParentElement = this.getButtonParent($currentTarget);
}
$(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
}; };
FilesCommentButton.prototype.buildButton = function(buttonAttributes) { FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
var initializedButtonTemplate; return $commentButtonTemplate.clone().attr({
initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
});
return $(initializedButtonTemplate).attr({
'data-noteable-type': buttonAttributes.noteableType, 'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID, 'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID, 'data-commit-id': buttonAttributes.commitID,
...@@ -86,14 +86,14 @@ ...@@ -86,14 +86,14 @@
}; };
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); return hoveredElement.closest(TEXT_FILE_SELECTOR);
}; };
FilesCommentButton.prototype.getLineContent = function(hoveredElement) { FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement; return hoveredElement;
} }
if (this.VIEW_TYPE === 'inline') { if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else { } else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS); return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
}; };
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
if (this.VIEW_TYPE === 'inline') { if (!this.isParallelView) {
if (hoveredElement.hasClass(OLD_LINE_CLASS)) { if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement; return hoveredElement;
} }
...@@ -114,17 +114,8 @@ ...@@ -114,17 +114,8 @@
} }
}; };
FilesCommentButton.prototype.isMovingToSameType = function(e) {
var newButtonParent;
newButtonParent = this.getButtonParent($(e.toElement));
if (!newButtonParent) {
return false;
}
return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
};
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
}; };
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
...@@ -135,6 +126,8 @@ ...@@ -135,6 +126,8 @@
})(); })();
$.fn.filesCommentButton = function() { $.fn.filesCommentButton = function() {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
if (!(this && (this.parent().data('can-create-note') != null))) { if (!(this && (this.parent().data('can-create-note') != null))) {
return; return;
} }
......
...@@ -48,7 +48,11 @@ ...@@ -48,7 +48,11 @@
} }
setOffset(offset = 0) { setOffset(offset = 0) {
this.dropdown.style.left = `${offset}px`; if (window.innerWidth > 480) {
this.dropdown.style.left = `${offset}px`;
} else {
this.dropdown.style.left = '0px';
}
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
} }
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
return BLUR_KEYCODES.indexOf(keyCode) >= 0; return BLUR_KEYCODES.indexOf(keyCode) !== -1;
}; };
GitLabDropdownFilter.prototype.filter = function(search_text) { GitLabDropdownFilter.prototype.filter = function(search_text) {
...@@ -605,7 +605,7 @@ ...@@ -605,7 +605,7 @@
var occurrences; var occurrences;
occurrences = fuzzaldrinPlus.match(text, term); occurrences = fuzzaldrinPlus.match(text, term);
return text.split('').map(function(character, i) { return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) >= 0) { if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>"; return "<b>" + character + "</b>";
} else { } else {
return character; return character;
...@@ -748,7 +748,7 @@ ...@@ -748,7 +748,7 @@
return function(e) { return function(e) {
var $listItems, PREV_INDEX, currentKeyCode; var $listItems, PREV_INDEX, currentKeyCode;
currentKeyCode = e.which; currentKeyCode = e.which;
if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
PREV_INDEX = currentIndex; PREV_INDEX = currentIndex;
......
require('./stat_graph_contributors_graph'); import ContributorsStatGraph from './stat_graph_contributors';
require('./stat_graph_contributors_util');
require('./stat_graph_contributors'); // export to global scope
require('./stat_graph'); window.ContributorsStatGraph = ContributorsStatGraph;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
StatGraph.log = {};
StatGraph.get_log = function() {
return this.log;
};
StatGraph.set_log = function(data) {
return this.log = data;
};
return StatGraph;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */
/* global ContributorsGraph */
/* global ContributorsAuthorGraph */
/* global ContributorsMasterGraph */
/* global ContributorsStatGraphUtil */
/* global d3 */
window.d3 = require('d3'); import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
(function() { export default (function() {
this.ContributorsStatGraph = (function() { function ContributorsStatGraph() {}
function ContributorsStatGraph() {}
ContributorsStatGraph.prototype.init = function(log) { ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits; var author_commits, total_commits;
this.parsed_log = ContributorsStatGraphUtil.parse_log(log); this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
this.set_current_field("commits"); this.set_current_field("commits");
total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
this.add_master_graph(total_commits); this.add_master_graph(total_commits);
this.add_authors_graph(author_commits); this.add_authors_graph(author_commits);
return this.change_date_header(); return this.change_date_header();
}; };
ContributorsStatGraph.prototype.add_master_graph = function(total_data) { ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
this.master_graph = new ContributorsMasterGraph(total_data); this.master_graph = new ContributorsMasterGraph(total_data);
return this.master_graph.draw(); return this.master_graph.draw();
}; };
ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
var limited_author_data; var limited_author_data;
this.authors = []; this.authors = [];
limited_author_data = author_data.slice(0, 100); limited_author_data = author_data.slice(0, 100);
return _.each(limited_author_data, (function(_this) { return _.each(limited_author_data, (function(_this) {
return function(d) { return function(d) {
var author_graph, author_header; var author_graph, author_header;
author_header = _this.create_author_header(d); author_header = _this.create_author_header(d);
$(".contributors-list").append(author_header); $(".contributors-list").append(author_header);
_this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
return author_graph.draw(); return author_graph.draw();
}; };
})(this)); })(this));
}; };
ContributorsStatGraph.prototype.format_author_commit_info = function(author) { ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
var commits; var commits;
commits = $('<span/>', { commits = $('<span/>', {
"class": 'graph-author-commits-count' "class": 'graph-author-commits-count'
}); });
commits.text(author.commits + " commits"); commits.text(author.commits + " commits");
return $('<span/>').append(commits); return $('<span/>').append(commits);
}; };
ContributorsStatGraph.prototype.create_author_header = function(author) { ContributorsStatGraph.prototype.create_author_header = function(author) {
var author_commit_info, author_commit_info_span, author_email, author_name, list_item; var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
list_item = $('<li/>', { list_item = $('<li/>', {
"class": 'person', "class": 'person',
style: 'display: block;' style: 'display: block;'
}); });
author_name = $('<h4>' + author.author_name + '</h4>'); author_name = $('<h4>' + author.author_name + '</h4>');
author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
author_commit_info_span = $('<span/>', { author_commit_info_span = $('<span/>', {
"class": 'commits' "class": 'commits'
}); });
author_commit_info = this.format_author_commit_info(author); author_commit_info = this.format_author_commit_info(author);
author_commit_info_span.html(author_commit_info); author_commit_info_span.html(author_commit_info);
list_item.append(author_name); list_item.append(author_name);
list_item.append(author_email); list_item.append(author_email);
list_item.append(author_commit_info_span); list_item.append(author_commit_info_span);
return list_item; return list_item;
}; };
ContributorsStatGraph.prototype.redraw_master = function() { ContributorsStatGraph.prototype.redraw_master = function() {
var total_data; var total_data;
total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
this.master_graph.set_data(total_data); this.master_graph.set_data(total_data);
return this.master_graph.redraw(); return this.master_graph.redraw();
}; };
ContributorsStatGraph.prototype.redraw_authors = function() { ContributorsStatGraph.prototype.redraw_authors = function() {
var author_commits, x_domain; var author_commits, x_domain;
$("ol").html(""); $("ol").html("");
x_domain = ContributorsGraph.prototype.x_domain; x_domain = ContributorsGraph.prototype.x_domain;
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
return _.each(author_commits, (function(_this) { return _.each(author_commits, (function(_this) {
return function(d) { return function(d) {
_this.redraw_author_commit_info(d); _this.redraw_author_commit_info(d);
$(_this.authors[d.author_name].list_item).appendTo("ol"); $(_this.authors[d.author_name].list_item).appendTo("ol");
_this.authors[d.author_name].set_data(d.dates); _this.authors[d.author_name].set_data(d.dates);
return _this.authors[d.author_name].redraw(); return _this.authors[d.author_name].redraw();
}; };
})(this)); })(this));
}; };
ContributorsStatGraph.prototype.set_current_field = function(field) { ContributorsStatGraph.prototype.set_current_field = function(field) {
return this.field = field; return this.field = field;
}; };
ContributorsStatGraph.prototype.change_date_header = function() { ContributorsStatGraph.prototype.change_date_header = function() {
var print, print_date_format, x_domain; var print, print_date_format, x_domain;
x_domain = ContributorsGraph.prototype.x_domain; x_domain = ContributorsGraph.prototype.x_domain;
print_date_format = d3.time.format("%B %e %Y"); print_date_format = d3.time.format("%B %e %Y");
print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
return $("#date_header").text(print); return $("#date_header").text(print);
}; };
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
var author_commit_info, author_list_item; var author_commit_info, author_list_item;
author_list_item = $(this.authors[author.author_name].list_item); author_list_item = $(this.authors[author.author_name].list_item);
author_commit_info = this.format_author_commit_info(author); author_commit_info = this.format_author_commit_info(author);
return author_list_item.find("span").html(author_commit_info); return author_list_item.find("span").html(author_commit_info);
}; };
return ContributorsStatGraph; return ContributorsStatGraph;
})(); })();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
(function() {
window.ContributorsStatGraphUtil = { export default {
parse_log: function(log) { parse_log: function(log) {
var by_author, by_email, data, entry, i, len, total, normalized_email; var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {}; total = {};
by_author = {}; by_author = {};
by_email = {}; by_email = {};
for (i = 0, len = log.length; i < len; i += 1) { for (i = 0, len = log.length; i < len; i += 1) {
entry = log[i]; entry = log[i];
if (total[entry.date] == null) { if (total[entry.date] == null) {
this.add_date(entry.date, total); this.add_date(entry.date, total);
}
normalized_email = entry.author_email.toLowerCase();
data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
if (!data[entry.date]) {
this.add_date(entry.date, data);
}
this.store_data(entry, total[entry.date], data[entry.date]);
}
total = _.toArray(total);
by_author = _.toArray(by_author);
return {
total: total,
by_author: by_author
};
},
add_date: function(date, collection) {
collection[date] = {};
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
by_email[normalized_email] = data;
return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
this.store_additions(entry, total, by_author);
return this.store_deletions(entry, total, by_author);
},
store_commits: function(total, by_author) {
this.add(total, "commits", 1);
return this.add(by_author, "commits", 1);
},
add: function(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
return collection[field] += value;
},
store_additions: function(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
} }
this.add(total, "additions", entry.additions); normalized_email = entry.author_email.toLowerCase();
return this.add(by_author, "additions", entry.additions); data = by_author[entry.author_name] || by_email[normalized_email];
}, if (data == null) {
store_deletions: function(entry, total, by_author) { data = this.add_author(entry, by_author, by_email);
if (entry.deletions == null) {
entry.deletions = 0;
} }
this.add(total, "deletions", entry.deletions); if (!data[entry.date]) {
return this.add(by_author, "deletions", entry.deletions); this.add_date(entry.date, data);
},
get_total_data: function(parsed_log, field) {
var log, total_data;
log = parsed_log.total;
total_data = this.pick_field(log, field);
return _.sortBy(total_data, function(d) {
return d.date;
});
},
pick_field: function(log, field) {
var total_data;
total_data = [];
_.each(log, function(d) {
return total_data.push(_.pick(d, [field, 'date']));
});
return total_data;
},
get_author_data: function(parsed_log, field, date_range) {
var author_data, log;
if (date_range == null) {
date_range = null;
}
log = parsed_log.by_author;
author_data = [];
_.each(log, (function(_this) {
return function(log_entry) {
var parsed_log_entry;
parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
if (!_.isEmpty(parsed_log_entry.dates)) {
return author_data.push(parsed_log_entry);
}
};
})(this));
return _.sortBy(author_data, function(d) {
return d[field];
}).reverse();
},
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
parsed_entry.dates[value.date] = value[field];
parsed_entry.commits += value.commits;
parsed_entry.additions += value.additions;
return parsed_entry.deletions += value.deletions;
}
};
})(this));
return parsed_entry;
},
in_range: function(date, date_range) {
var ref;
if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
return true;
} else {
return false;
} }
this.store_data(entry, total[entry.date], data[entry.date]);
}
total = _.toArray(total);
by_author = _.toArray(by_author);
return {
total: total,
by_author: by_author
};
},
add_date: function(date, collection) {
collection[date] = {};
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
by_email[normalized_email] = data;
return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
this.store_additions(entry, total, by_author);
return this.store_deletions(entry, total, by_author);
},
store_commits: function(total, by_author) {
this.add(total, "commits", 1);
return this.add(by_author, "commits", 1);
},
add: function(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
return collection[field] += value;
},
store_additions: function(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
}
this.add(total, "additions", entry.additions);
return this.add(by_author, "additions", entry.additions);
},
store_deletions: function(entry, total, by_author) {
if (entry.deletions == null) {
entry.deletions = 0;
}
this.add(total, "deletions", entry.deletions);
return this.add(by_author, "deletions", entry.deletions);
},
get_total_data: function(parsed_log, field) {
var log, total_data;
log = parsed_log.total;
total_data = this.pick_field(log, field);
return _.sortBy(total_data, function(d) {
return d.date;
});
},
pick_field: function(log, field) {
var total_data;
total_data = [];
_.each(log, function(d) {
return total_data.push(_.pick(d, [field, 'date']));
});
return total_data;
},
get_author_data: function(parsed_log, field, date_range) {
var author_data, log;
if (date_range == null) {
date_range = null;
}
log = parsed_log.by_author;
author_data = [];
_.each(log, (function(_this) {
return function(log_entry) {
var parsed_log_entry;
parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
if (!_.isEmpty(parsed_log_entry.dates)) {
return author_data.push(parsed_log_entry);
}
};
})(this));
return _.sortBy(author_data, function(d) {
return d[field];
}).reverse();
},
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
parsed_entry.dates[value.date] = value[field];
parsed_entry.commits += value.commits;
parsed_entry.additions += value.additions;
return parsed_entry.deletions += value.deletions;
}
};
})(this));
return parsed_entry;
},
in_range: function(date, date_range) {
var ref;
if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
return true;
} else {
return false;
} }
}; }
}).call(window); };
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
formData = $.param(formData); formData = $.param(formData);
formAction = form.attr('action'); formAction = form.attr('action');
issuesUrl = formAction; issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
issuesUrl += formData; issuesUrl += formData;
return gl.utils.visitUrl(issuesUrl); return gl.utils.visitUrl(issuesUrl);
}; };
......
...@@ -329,17 +329,18 @@ ...@@ -329,17 +329,18 @@
* ``` * ```
*/ */
w.gl.utils.backOff = (fn, timeout = 60000) => { w.gl.utils.backOff = (fn, timeout = 60000) => {
const maxInterval = 32000;
let nextInterval = 2000; let nextInterval = 2000;
const startTime = (+new Date()); const startTime = Date.now();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => { const next = () => {
if (new Date().getTime() - startTime < timeout) { if (Date.now() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), nextInterval); setTimeout(fn.bind(null, next, stop), nextInterval);
nextInterval *= 2; nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else { } else {
reject(new Error('BACKOFF_TIMEOUT')); reject(new Error('BACKOFF_TIMEOUT'));
} }
......
...@@ -7,4 +7,4 @@ const statusCodes = { ...@@ -7,4 +7,4 @@ const statusCodes = {
OK: 200, OK: 200,
}; };
export default statusCodes; module.exports = statusCodes;
...@@ -82,7 +82,7 @@ require('./smart_interval'); ...@@ -82,7 +82,7 @@ require('./smart_interval');
return function() { return function() {
var page; var page;
page = $('body').data('page').split(':').last(); page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) { if (allowedPages.indexOf(page) === -1) {
return _this.clearEventListeners(); return _this.clearEventListeners();
} }
}; };
...@@ -255,7 +255,7 @@ require('./smart_interval'); ...@@ -255,7 +255,7 @@ require('./smart_interval');
} }
$('.ci_widget').hide(); $('.ci_widget').hide();
allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
if (indexOf.call(allowed_states, state) >= 0) { if (indexOf.call(allowed_states, state) !== -1) {
$('.ci_widget.ci-' + state).show(); $('.ci_widget.ci-' + state).show();
switch (state) { switch (state) {
case "failed": case "failed":
......
...@@ -78,7 +78,6 @@ ...@@ -78,7 +78,6 @@
} else { } else {
$(element).find('.assignee-icon').empty(); $(element).find('.assignee-icon').empty();
} }
return $(element).effect('highlight');
}; };
function Milestone() { function Milestone() {
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
$value = $block.find('.value'); $value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
} }
...@@ -181,8 +181,7 @@ ...@@ -181,8 +181,7 @@
$selectbox.hide(); $selectbox.hide();
$value.css('display', ''); $value.css('display', '');
if (data.milestone != null) { if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace; data.milestone.full_path = _this.currentProject.full_path;
data.milestone.path = _this.currentProject.path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
...@@ -20,15 +20,35 @@ ...@@ -20,15 +20,35 @@
}; };
NewBranchForm.prototype.init = function() { NewBranchForm.prototype.init = function() {
if (this.name.val().length > 0) { if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur'); return this.name.trigger('blur');
} }
}; };
NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
return this.ref.autocomplete({ var $branchSelect = $('.js-branch-select');
source: availableRefs,
minLength: 1 $branchSelect.glDropdown({
data: availableRefs,
filterable: true,
filterByText: true,
remote: false,
fieldName: $branchSelect.data('field-name'),
selectable: true,
isSelectable: function(branch, $el) {
return !$el.hasClass('is-active');
},
text: function(branch) {
return branch;
},
id: function(branch) {
return branch;
},
toggleLabel: function(branch) {
if (branch) {
return branch;
}
}
}); });
}; };
...@@ -61,7 +81,7 @@ ...@@ -61,7 +81,7 @@
var errorMessage, errors, formatter, unique, validator; var errorMessage, errors, formatter, unique, validator;
this.branchNameError.empty(); this.branchNameError.empty();
unique = function(values, value) { unique = function(values, value) {
if (indexOf.call(values, value) < 0) { if (indexOf.call(values, value) === -1) {
values.push(value); values.push(value);
} }
return values; return values;
......
...@@ -84,13 +84,14 @@ ...@@ -84,13 +84,14 @@
} }
$(function() { $(function() {
$(document).on('focusout.ssh_key', '#key_key', function() { $(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title'); const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') {
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
return $title.val(comment[1]).change(); return $title.val(comment[1]).change();
} }
// Extract the SSH Key title from its comment
}); });
if (global.utils.getPagePath() === 'profiles') { if (global.utils.getPagePath() === 'profiles') {
return new Profile(); return new Profile();
......
// require everything else in this directory require('./gl_crop');
function requireAll(context) { return context.keys().map(context); } require('./profile');
requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
...@@ -123,7 +123,7 @@ ...@@ -123,7 +123,7 @@
if ($('input[name="ref"]').length) { if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form'); var $form = $dropdown.closest('form');
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') < 0 ? '?' : '&'; var divider = action.indexOf('?') === -1 ? '?' : '&';
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
} }
} }
......
...@@ -79,7 +79,6 @@ ...@@ -79,7 +79,6 @@
protected_branch: formData protected_branch: formData
}, },
success: (response) => { success: (response) => {
this.$wrap.effect('highlight');
this.hasChanges = false; this.hasChanges = false;
for (const ACCESS_LEVEL in ACCESS_LEVELS) { for (const ACCESS_LEVEL in ACCESS_LEVELS) {
...@@ -93,6 +92,9 @@ ...@@ -93,6 +92,9 @@
$.scrollTo(0); $.scrollTo(0);
new Flash('Failed to update branch!'); new Flash('Failed to update branch!');
} }
}).always(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
}); });
} }
......
// require everything else in this directory require('./protected_branch_access_dropdown');
function requireAll(context) { return context.keys().map(context); } require('./protected_branch_create');
requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); require('./protected_branch_dropdown');
require('./protected_branch_edit');
require('./protected_branch_edit_list');
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */ /* global ace */
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
(function() { (function() {
$(function() { $(function() {
var editor = ace.edit("editor"); var editor = ace.edit("editor");
......
/* global Flash */
require('vendor/task_list'); require('vendor/task_list');
class TaskList { class TaskList {
...@@ -6,6 +7,16 @@ class TaskList { ...@@ -6,6 +7,16 @@ class TaskList {
this.dataType = options.dataType; this.dataType = options.dataType;
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {}); this.onSuccess = options.onSuccess || (() => {});
this.onError = function showFlash(response) {
let errorMessages = '';
if (response.responseJSON) {
errorMessages = response.responseJSON.errors.join(' ');
}
return new Flash(errorMessages || 'Update failed', 'alert');
};
this.init(); this.init();
} }
...@@ -32,6 +43,7 @@ class TaskList { ...@@ -32,6 +43,7 @@ class TaskList {
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
data: patchData, data: patchData,
success: this.onSuccess, success: this.onSuccess,
error: this.onError,
}); });
} }
} }
......
/* global Cookies */
const userCalloutElementName = '.user-callout';
const closeButton = '.close-user-callout';
const userCalloutBtn = '.user-callout-btn';
const userCalloutSvgAttrName = 'callout-svg';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
const USER_CALLOUT_TEMPLATE = `
<div class="bordered-box landing content-block">
<button class="btn btn-default close close-user-callout" type="button">
<i class="fa fa-times dismiss-icon"></i>
</button>
<div class="row">
<div class="col-sm-3 col-xs-12 svg-container">
</div>
<div class="col-sm-8 col-xs-12 inner-content">
<h4>
Customize your experience
</h4>
<p>
Change syntax themes, default project pages, and more in preferences.
</p>
<a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
</div>
</div>
</div>`;
class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName);
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.init();
}
init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg);
this.userCalloutBody.append($template);
$template.find(closeButton).on('click', e => this.dismissCallout(e));
$template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
this.userCalloutBody.empty();
}
}
}
module.exports = UserCallout;
// require everything else in this directory require('./calendar');
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
/* global Vue, Flash, gl */ /* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign, no-alert */
((gl) => { ((gl) => {
gl.VuePipelineActions = Vue.extend({ gl.VuePipelineActions = Vue.extend({
...@@ -16,21 +16,33 @@ ...@@ -16,21 +16,33 @@
download(name) { download(name) {
return `Download ${name} artifacts`; return `Download ${name} artifacts`;
}, },
/**
* Shows a dialog when the user clicks in the cancel button.
* We need to prevent the default behavior and stop propagation because the
* link relies on UJS.
*
* @param {Event} event
*/
confirmAction(event) {
if (!confirm('Are you sure you want to cancel this pipeline?')) {
event.preventDefault();
event.stopPropagation();
}
},
}, },
template: ` template: `
<td class="pipeline-actions hidden-xs"> <td class="pipeline-actions hidden-xs">
<div class="controls pull-right"> <div class="pull-right">
<div class="btn-group inline"> <div class="btn-group">
<div class="btn-group"> <div class="btn-group" v-if="actions">
<button <button
v-if='actions'
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown" data-toggle="dropdown"
title="Manual job" title="Manual job"
data-placement="top" data-placement="top"
aria-label="Manual job" aria-label="Manual job">
> <span v-html="svgs.iconPlay" aria-hidden="true"></span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
...@@ -38,23 +50,21 @@ ...@@ -38,23 +50,21 @@
<a <a
rel="nofollow" rel="nofollow"
data-method="post" data-method="post"
:href='action.path' :href="action.path">
> <span v-html="svgs.iconPlay" aria-hidden="true"></span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<span>{{action.name}}</span> <span>{{action.name}}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="btn-group">
<div class="btn-group" v-if="artifacts">
<button <button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts" title="Artifacts"
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Artifacts" aria-label="Artifacts">
>
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
...@@ -62,41 +72,39 @@ ...@@ -62,41 +72,39 @@
<li v-for='artifact in pipeline.details.artifacts'> <li v-for='artifact in pipeline.details.artifacts'>
<a <a
rel="nofollow" rel="nofollow"
download :href="artifact.path">
:href='artifact.path'
>
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span> <span>{{download(artifact.name)}}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> <div class="btn-group" v-if="pipeline.flags.retryable">
<div class="cancel-retry-btns inline"> <a
<a class="btn btn-default btn-retry has-tooltip"
v-if='pipeline.flags.retryable' title="Retry"
class="btn has-tooltip" rel="nofollow"
title="Retry" data-method="post"
rel="nofollow" data-placement="top"
data-method="post" data-toggle="dropdown"
data-placement="top" :href='pipeline.retry_path'
data-toggle="dropdown" aria-label="Retry">
:href='pipeline.retry_path' <i class="fa fa-repeat" aria-hidden="true"></i>
aria-label="Retry"> </a>
<i class="fa fa-repeat" aria-hidden="true"></i> </div>
</a> <div class="btn-group" v-if="pipeline.flags.cancelable">
<a <a
v-if='pipeline.flags.cancelable' class="btn btn-remove has-tooltip"
class="btn btn-remove has-tooltip" title="Cancel"
title="Cancel" rel="nofollow"
rel="nofollow" data-method="post"
data-method="post" data-placement="top"
data-placement="top" data-toggle="dropdown"
data-toggle="dropdown" :href='pipeline.cancel_path'
:href='pipeline.cancel_path' aria-label="Cancel">
aria-label="Cancel"> <i class="fa fa-remove" aria-hidden="true"></i>
<i class="fa fa-remove" aria-hidden="true"></i> </a>
</a> </div>
</div> </div>
</div> </div>
</td> </td>
......
...@@ -23,6 +23,13 @@ ...@@ -23,6 +23,13 @@
required: true, required: true,
}, },
}, },
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: { methods: {
fetchBuilds(e) { fetchBuilds(e) {
const areaExpanded = e.currentTarget.attributes['aria-expanded']; const areaExpanded = e.currentTarget.attributes['aria-expanded'];
...@@ -37,17 +44,19 @@ ...@@ -37,17 +44,19 @@
return flash; return flash;
}); });
}, },
keepGraph(e) {
const { target } = e;
if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
if (
target.parentElement &&
(target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
) return null;
return e.stopPropagation(); /**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
}, },
}, },
computed: { computed: {
...@@ -76,13 +85,13 @@ ...@@ -76,13 +85,13 @@
template: ` template: `
<div> <div>
<button <button
@click='fetchBuilds($event)' @click="fetchBuilds($event)"
:class="triggerButtonClass" :class="triggerButtonClass"
:title='stage.title' :title="stage.title"
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
type="button" type="button"
:aria-label='stage.title' :aria-label="stage.title"
> >
<span v-html="svg" aria-hidden="true"></span> <span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
...@@ -90,7 +99,6 @@ ...@@ -90,7 +99,6 @@
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div> <div class="arrow-up" aria-hidden="true"></div>
<div <div
@click='keepGraph($event)'
:class="dropdownClass" :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner" v-html="buildsOrSpinner"
......
...@@ -54,7 +54,7 @@ require('../lib/utils/datetime_utility'); ...@@ -54,7 +54,7 @@ require('../lib/utils/datetime_utility');
}, },
}, },
template: ` template: `
<td> <td class="pipelines-time-ago">
<p class="duration" v-if='duration'> <p class="duration" v-if='duration'>
<span v-html='svgs.iconTimer'></span> <span v-html='svgs.iconTimer'></span>
{{duration}} {{duration}}
...@@ -65,8 +65,7 @@ require('../lib/utils/datetime_utility'); ...@@ -65,8 +65,7 @@ require('../lib/utils/datetime_utility');
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:data-original-title='localTimeFinished' :data-original-title='localTimeFinished'>
>
{{timeStopped.words}} {{timeStopped.words}}
</time> </time>
</p> </p>
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory * This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope. * the top of the compiled file, but it's generally better to create a new file per style scope.
*= require jquery-ui/autocomplete
*= require jquery.atwho *= require jquery.atwho
*= require select2 *= require select2
*= require_self *= require_self
......
...@@ -229,7 +229,7 @@ ...@@ -229,7 +229,7 @@
.controls { .controls {
float: right; float: right;
margin-top: 8px; margin-top: 8px;
padding-bottom: 7px; padding-bottom: 8px;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -26,6 +26,11 @@ ...@@ -26,6 +26,11 @@
.filtered-search-container { .filtered-search-container {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
@media (max-width: $screen-xs-min) {
-webkit-flex-direction: column;
flex-direction: column;
}
} }
.filtered-search-input-container { .filtered-search-input-container {
...@@ -34,6 +39,20 @@ ...@@ -34,6 +39,20 @@
position: relative; position: relative;
width: 100%; width: 100%;
@media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%;
flex: 1 1 100%;
margin-bottom: 10px;
.dropdown-menu {
width: auto;
left: 0;
right: 0;
max-width: none;
min-width: 100%;
}
}
.form-control { .form-control {
padding-left: 25px; padding-left: 25px;
padding-right: 25px; padding-right: 25px;
...@@ -79,6 +98,31 @@ ...@@ -79,6 +98,31 @@
overflow: auto; overflow: auto;
} }
@media (max-width: $screen-xs-min) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
border-top: 0;
}
.filter-dropdown-container {
.dropdown-toggle,
.dropdown {
width: 100%;
}
.dropdown {
margin-left: 0;
}
.fa-chevron-down {
position: absolute;
right: 10px;
top: 10px;
}
}
}
%filter-dropdown-item-btn-hover { %filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color; background-color: $dropdown-hover-color;
color: $white-light; color: $white-light;
...@@ -148,4 +192,4 @@ ...@@ -148,4 +192,4 @@
.filter-dropdown-loading { .filter-dropdown-loading {
padding: 8px 16px; padding: 8px 16px;
} }
\ No newline at end of file
...@@ -2,17 +2,6 @@ ...@@ -2,17 +2,6 @@
font-family: $regular_font; font-family: $regular_font;
font-size: $font-size-base; font-size: $font-size-base;
&.ui-autocomplete {
border-color: $jq-ui-border;
padding: 0;
margin-top: 2px;
z-index: 1001;
.ui-menu-item a {
padding: 4px 10px;
}
}
.ui-state-default { .ui-state-default {
border: 1px solid $white-light; border: 1px solid $white-light;
background: $white-light; background: $white-light;
......
...@@ -29,16 +29,14 @@ ...@@ -29,16 +29,14 @@
} }
} }
@media (min-width: $screen-sm-min) {
.content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
.right-sidebar-collapsed { .right-sidebar-collapsed {
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.content-wrapper {
padding-right: $gutter_collapsed_width;
}
.merge-request-tabs-holder.affix { .merge-request-tabs-holder.affix {
right: $gutter_collapsed_width; right: $gutter_collapsed_width;
} }
...@@ -56,6 +54,12 @@ ...@@ -56,6 +54,12 @@
.right-sidebar-expanded { .right-sidebar-expanded {
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
.content-wrapper { .content-wrapper {
padding-right: $gutter_width; padding-right: $gutter_width;
......
...@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792; ...@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792;
$dark-highlight-color: $black; $dark-highlight-color: $black;
$dark-pre-hll-bg: #373b41; $dark-pre-hll-bg: #373b41;
$dark-hll-bg: #373b41; $dark-hll-bg: #373b41;
$dark-over-bg: #9f9ab5;
$dark-c: #969896; $dark-c: #969896;
$dark-err: #c66; $dark-err: #c66;
$dark-k: #b294bb; $dark-k: #b294bb;
...@@ -139,6 +140,18 @@ $dark-il: #de935f; ...@@ -139,6 +140,18 @@ $dark-il: #de935f;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $dark-over-bg;
border-color: darken($dark-over-bg, 5%);
a {
color: darken($dark-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e; ...@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080; $monokai-diff-border: #808080;
$monokai-highlight-bg: #ffe792; $monokai-highlight-bg: #ffe792;
$monokai-over-bg: #9f9ab5;
$monokai-new-bg: rgba(166, 226, 46, 0.1); $monokai-new-bg: rgba(166, 226, 46, 0.1);
$monokai-new-idiff: rgba(166, 226, 46, 0.15); $monokai-new-idiff: rgba(166, 226, 46, 0.15);
...@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e; ...@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $monokai-over-bg;
border-color: darken($monokai-over-bg, 5%);
a {
color: darken($monokai-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c; ...@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c;
$solarized-dark-line-color-old: #7a6c71; $solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554; $solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652; $solarized-dark-hll-bg: #174652;
$solarized-dark-over-bg: #9f9ab5;
$solarized-dark-c: #586e75; $solarized-dark-c: #586e75;
$solarized-dark-err: #93a1a1; $solarized-dark-err: #93a1a1;
$solarized-dark-g: #93a1a1; $solarized-dark-g: #93a1a1;
...@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198; ...@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-dark-over-bg;
border-color: darken($solarized-dark-over-bg, 5%);
a {
color: darken($solarized-dark-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080; ...@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080;
$solarized-light-line-color-old: #ad9186; $solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5; $solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5; $solarized-light-hll-bg: #ddd8c5;
$solarized-light-over-bg: #ded7fc;
$solarized-light-c: #93a1a1; $solarized-light-c: #93a1a1;
$solarized-light-err: #586e75; $solarized-light-err: #586e75;
$solarized-light-g: #586e75; $solarized-light-g: #586e75;
...@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198; ...@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-light-over-bg;
border-color: darken($solarized-light-over-bg, 5%);
a {
color: darken($solarized-light-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include matchLine; @include matchLine;
} }
......
...@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color; ...@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color;
$white-highlight: #fafe3d; $white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7; $white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8; $white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-c: #998; $white-c: #998;
$white-err: #a61717; $white-err: #a61717;
$white-err-bg: #e3d2d2; $white-err-bg: #e3d2d2;
...@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5; ...@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5;
} }
} }
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) { &.hll:not(.empty-cell) {
background-color: $line-number-select; background-color: $line-number-select;
border-color: $line-select-yellow-dark; border-color: $line-select-yellow-dark;
......
...@@ -89,6 +89,10 @@ ...@@ -89,6 +89,10 @@
.diff-line-num { .diff-line-num {
width: 50px; width: 50px;
a {
transition: none;
}
} }
.line_holder td { .line_holder td {
...@@ -109,10 +113,6 @@ ...@@ -109,10 +113,6 @@
td.line_content.parallel { td.line_content.parallel {
width: 46%; width: 46%;
} }
.add-diff-note {
margin-left: -65px;
}
} }
.old_line, .old_line,
......
...@@ -452,36 +452,37 @@ ul.notes { ...@@ -452,36 +452,37 @@ ul.notes {
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.diff-file tr.line_holder { .add-diff-note {
@mixin show-add-diff-note { display: none;
display: inline-block; margin-top: -2px;
} border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $gl-link-color;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $border-color;
transition: transform .1s ease-in-out;
.add-diff-note { &:hover {
margin-top: -8px; background: $gl-info;
border-radius: 40px; color: $white-light;
background: $white-light; transform: scale(1.15);
padding: 4px; }
font-size: 16px;
color: $gl-link-color;
margin-left: -56px;
position: absolute;
z-index: 10;
width: 32px;
// "hide" it by default
display: none;
&:hover { &:active {
background: $gl-info; outline: 0;
color: $white-light;
@include show-add-diff-note;
}
} }
}
// "show" the icon also if we just hover somewhere over the line .diff-file {
&:hover > td { .is-over {
.add-diff-note { .add-diff-note {
@include show-add-diff-note; display: inline-block;
} }
} }
} }
......
...@@ -13,21 +13,16 @@ ...@@ -13,21 +13,16 @@
white-space: nowrap; white-space: nowrap;
} }
.commit-title { .table-holder {
margin: 0; width: 100%;
} overflow: auto;
.controls {
white-space: nowrap;
} }
.btn { .commit-title {
margin: 4px; margin: 0;
} }
.table.ci-table { .table.ci-table {
min-width: 1200px;
table-layout: fixed;
.label { .label {
margin-bottom: 3px; margin-bottom: 3px;
...@@ -37,16 +32,72 @@ ...@@ -37,16 +32,72 @@
color: $black; color: $black;
} }
.pipeline-date, .stage-cell {
.pipeline-status { min-width: 130px; // Guarantees we show at least 4 stages in line
width: 10%; width: 20%;
}
.pipelines-time-ago {
text-align: right;
} }
.pipeline-info,
.pipeline-commit,
.pipeline-stages,
.pipeline-actions { .pipeline-actions {
width: 20%; padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default {
color: $gl-text-color-secondary;
}
.btn.btn-retry:hover,
.btn.btn-retry:focus {
border-color: $gray-darkest;
background-color: $white-normal;
}
svg path {
fill: $gl-text-color-secondary;
}
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle,
.dropdown-menu {
color: $gl-text-color-secondary;
.fa {
color: $gl-text-color-secondary;
font-size: 14px;
}
svg,
.fa {
margin-right: 0;
}
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
.tooltip {
white-space: nowrap;
}
} }
} }
} }
...@@ -61,27 +112,10 @@ ...@@ -61,27 +112,10 @@
} }
} }
.content-list.pipelines .table-holder {
min-height: 300px;
}
.pipeline-holder {
width: 100%;
overflow: auto;
}
.table.ci-table { .table.ci-table {
min-width: 900px;
&.pipeline {
min-width: 650px;
}
&.builds-page {
tr { &.builds-page tr {
height: 71px; height: 71px;
}
} }
tr { tr {
...@@ -94,6 +128,10 @@ ...@@ -94,6 +128,10 @@
padding: 10px 8px; padding: 10px 8px;
} }
td.environments-actions {
padding-right: 0;
}
td.stage-cell { td.stage-cell {
padding: 10px 0; padding: 10px 0;
} }
...@@ -103,7 +141,7 @@ ...@@ -103,7 +141,7 @@
} }
.commit-link { .commit-link {
padding: 9px 8px 10px; padding: 9px 8px 10px 2px;
} }
} }
...@@ -210,72 +248,8 @@ ...@@ -210,72 +248,8 @@
} }
} }
.pipeline-actions { .build-link a {
min-width: 140px; color: $gl-text-color;
.btn {
margin: 0;
color: $gl-text-color-secondary;
}
.cancel-retry-btns {
vertical-align: middle;
.btn:not(:first-child) {
margin-left: 8px;
}
}
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle,
.dropdown-menu {
color: $gl-text-color-secondary;
.fa {
color: $gl-text-color-secondary;
font-size: 14px;
}
svg,
.fa {
margin-right: 0;
}
}
.btn-remove {
color: $white-light;
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
.tooltip {
white-space: nowrap;
}
}
.build-link {
a {
color: $gl-text-color;
}
} }
.btn-group.open .dropdown-toggle { .btn-group.open .dropdown-toggle {
...@@ -339,31 +313,8 @@ ...@@ -339,31 +313,8 @@
} }
.tab-pane { .tab-pane {
&.pipelines { &.builds .ci-table tr {
.ci-table { height: 71px;
min-width: 900px;
}
.content-list.pipelines {
overflow: auto;
}
.stage {
max-width: 100px;
width: 100px;
}
.pipeline-actions {
min-width: initial;
}
}
&.builds {
.ci-table {
tr {
height: 71px;
}
}
} }
} }
......
...@@ -277,3 +277,41 @@ table.u2f-registrations { ...@@ -277,3 +277,41 @@ table.u2f-registrations {
padding-left: 18px; padding-left: 18px;
} }
} }
.user-callout {
margin: 24px auto 0;
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.landing {
margin-bottom: $gl-padding;
.close {
margin-right: 20px;
}
.dismiss-icon {
float: right;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
text-align: center;
svg {
width: 136px;
height: 136px;
}
}
}
@media(max-width: $screen-xs-max) {
.inner-content {
padding-left: 30px;
}
}
}
...@@ -638,14 +638,6 @@ pre.light-well { ...@@ -638,14 +638,6 @@ pre.light-well {
margin: 0; margin: 0;
} }
.activity-filter-block {
.controls {
padding-bottom: 7px;
margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
.commits-search-form { .commits-search-form {
.input-short { .input-short {
min-width: 200px; min-width: 200px;
......
...@@ -92,6 +92,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -92,6 +92,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:container_registry_token_expire_delay, :container_registry_token_expire_delay,
:default_artifacts_expire_in,
:default_branch_protection, :default_branch_protection,
:default_group_visibility, :default_group_visibility,
:default_project_visibility, :default_project_visibility,
......
...@@ -26,6 +26,23 @@ module IssuableActions ...@@ -26,6 +26,23 @@ module IssuableActions
private private
def render_conflict_response
respond_to do |format|
format.html do
@conflict = true
render :edit
end
format.json do
render json: {
errors: [
"Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
]
}, status: 409
end
end
end
def labels def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end end
......
class Projects::BranchesController < Projects::ApplicationController class Projects::BranchesController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
include SortingHelper include SortingHelper
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project, except: :create
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged] before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
...@@ -32,6 +33,8 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -32,6 +33,8 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name])) branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name) branch_name = Addressable::URI.unescape(branch_name)
redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
result = CreateBranchService.new(project, current_user). result = CreateBranchService.new(project, current_user).
execute(branch_name, ref) execute(branch_name, ref)
...@@ -42,8 +45,15 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -42,8 +45,15 @@ class Projects::BranchesController < Projects::ApplicationController
if result[:status] == :success if result[:status] == :success
@branch = result[:branch] @branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project,
@branch.name) if redirect_to_autodeploy
redirect_to(
url_to_autodeploy_setup(project, branch_name),
notice: view_context.autodeploy_flash_notice(branch_name))
else
redirect_to namespace_project_tree_path(@project.namespace, @project,
@branch.name)
end
else else
@error = result[:message] @error = result[:message]
render action: 'new' render action: 'new'
...@@ -76,7 +86,19 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -76,7 +86,19 @@ class Projects::BranchesController < Projects::ApplicationController
ref_escaped = sanitize(strip_tags(params[:ref])) ref_escaped = sanitize(strip_tags(params[:ref]))
Addressable::URI.unescape(ref_escaped) Addressable::URI.unescape(ref_escaped)
else else
@project.default_branch @project.default_branch || 'master'
end end
end end
def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path(
project.namespace,
project,
branch_name,
file_name: '.gitlab-ci.yml',
commit_message: 'Set up auto deploy',
target_branch: branch_name,
context: 'autodeploy'
)
end
end end
...@@ -139,8 +139,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -139,8 +139,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
@conflict = true render_conflict_response
render :edit
end end
def referenced_merge_requests def referenced_merge_requests
......
...@@ -305,24 +305,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -305,24 +305,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request = MergeRequests::UpdateService.new(project, current_user, update_params).execute(@merge_request) @merge_request = MergeRequests::UpdateService.new(project, current_user, update_params).execute(@merge_request)
if @merge_request.valid? respond_to do |format|
respond_to do |format| format.html do
format.html do if @merge_request.valid?
redirect_to([@merge_request.target_project.namespace.becomes(Namespace), redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
@merge_request.target_project, @merge_request]) else
end set_suggested_approvers
format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) render :edit
end end
end end
else
set_suggested_approvers format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
render "edit" end
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
@conflict = true render_conflict_response
render :edit
end end
def remove_wip def remove_wip
......
...@@ -15,4 +15,11 @@ module BuildsHelper ...@@ -15,4 +15,11 @@ module BuildsHelper
log_state: @build.trace_with_state[:state].to_s log_state: @build.trace_with_state[:state].to_s
} }
end end
def build_failed_issue_options
{
title: "Build Failed ##{@build.id}",
description: namespace_project_build_url(@project.namespace, @project, @build)
}
end
end end
...@@ -34,7 +34,7 @@ module ButtonHelper ...@@ -34,7 +34,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol, content_tag (append_link ? :a : :span), protocol,
class: klass, class: klass,
href: (project.http_url_to_repo if append_link), href: (project.http_url_to_repo(current_user) if append_link),
data: { data: {
html: true, html: true,
placement: placement, placement: placement,
......
...@@ -52,7 +52,7 @@ module IssuablesHelper ...@@ -52,7 +52,7 @@ module IssuablesHelper
field_name: 'issuable_template', field_name: 'issuable_template',
selected: selected_template(issuable), selected: selected_template(issuable),
project_path: ref_project.path, project_path: ref_project.path,
namespace_path: ref_project.namespace.path namespace_path: ref_project.namespace.full_path
} }
} }
......
...@@ -150,6 +150,15 @@ module ProjectsHelper ...@@ -150,6 +150,15 @@ module ProjectsHelper
).html_safe ).html_safe
end end
def link_to_autodeploy_doc
link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank'
end
def autodeploy_flash_notice(branch_name)
"Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \
choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe
end
private private
def repo_children_classes(field) def repo_children_classes(field)
...@@ -244,7 +253,7 @@ module ProjectsHelper ...@@ -244,7 +253,7 @@ module ProjectsHelper
when 'ssh' when 'ssh'
project.ssh_url_to_repo project.ssh_url_to_repo
else else
project.http_url_to_repo project.http_url_to_repo(current_user)
end end
end end
......
...@@ -81,6 +81,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -81,6 +81,12 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :max_artifacts_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_registry_token_expire_delay, validates :container_registry_token_expire_delay,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
...@@ -187,6 +193,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -187,6 +193,7 @@ class ApplicationSetting < ActiveRecord::Base
after_sign_up_text: nil, after_sign_up_text: nil,
akismet_enabled: false, akismet_enabled: false,
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'], default_projects_limit: Settings.gitlab['default_projects_limit'],
...@@ -220,9 +227,9 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -220,9 +227,9 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil, sign_in_text: nil,
signin_enabled: Settings.gitlab['signin_enabled'], signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, user_default_external: false
terminal_max_session_time: 0
} }
end end
...@@ -243,6 +250,14 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -243,6 +250,14 @@ class ApplicationSetting < ActiveRecord::Base
create(defaults) create(defaults)
end end
def self.human_attribute_name(attr, _options = {})
if attr == :default_artifacts_expire_in
'Default artifacts expiration'
else
super
end
end
def update_mirror_cron_jobs def update_mirror_cron_jobs
Project.mirror.where('sync_time < ?', minimum_mirror_sync_time) Project.mirror.where('sync_time < ?', minimum_mirror_sync_time)
.update_all(sync_time: minimum_mirror_sync_time) .update_all(sync_time: minimum_mirror_sync_time)
......
...@@ -485,7 +485,7 @@ module Ci ...@@ -485,7 +485,7 @@ module Ci
def artifacts_expire_in=(value) def artifacts_expire_in=(value)
self.artifacts_expire_at = self.artifacts_expire_at =
if value if value
Time.now + ChronicDuration.parse(value) ChronicDuration.parse(value)&.seconds&.from_now
end end
end end
......
...@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base ...@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) } scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do scope :in_projects, ->(projects) do
where(project_id: projects).recent where(project_id: projects.pluck(:id)).recent
end end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
......
...@@ -21,8 +21,6 @@ class Issue < ActiveRecord::Base ...@@ -21,8 +21,6 @@ class Issue < ActiveRecord::Base
DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
ActsAsTaggableOn.strict_case_match = true
belongs_to :project belongs_to :project
belongs_to :moved_to, class_name: 'Issue' belongs_to :moved_to, class_name: 'Issue'
......
...@@ -64,8 +64,7 @@ class Project < ActiveRecord::Base ...@@ -64,8 +64,7 @@ class Project < ActiveRecord::Base
after_validation :check_pending_delete after_validation :check_pending_delete
ActsAsTaggableOn.strict_case_match = true acts_as_taggable
acts_as_taggable_on :tags
attr_accessor :new_default_branch attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
...@@ -399,7 +398,7 @@ class Project < ActiveRecord::Base ...@@ -399,7 +398,7 @@ class Project < ActiveRecord::Base
end end
def reference_pattern def reference_pattern
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{ %r{
((?<namespace>#{name_pattern})\/)? ((?<namespace>#{name_pattern})\/)?
...@@ -958,10 +957,6 @@ class Project < ActiveRecord::Base ...@@ -958,10 +957,6 @@ class Project < ActiveRecord::Base
gitlab_shell.url_to_repo(path_with_namespace) gitlab_shell.url_to_repo(path_with_namespace)
end end
def namespace_dir
namespace.try(:path) || ''
end
def repo_exists? def repo_exists?
@repo_exists ||= repository.exists? @repo_exists ||= repository.exists?
rescue rescue
...@@ -984,8 +979,14 @@ class Project < ActiveRecord::Base ...@@ -984,8 +979,14 @@ class Project < ActiveRecord::Base
url_to_repo url_to_repo
end end
def http_url_to_repo def http_url_to_repo(user = nil)
"#{web_url}.git" url = web_url
if user
url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
end
"#{url}.git"
end end
# No need to have a Kerberos Web url. Kerberos URL will be used only to clone # No need to have a Kerberos Web url. Kerberos URL will be used only to clone
...@@ -1015,8 +1016,8 @@ class Project < ActiveRecord::Base ...@@ -1015,8 +1016,8 @@ class Project < ActiveRecord::Base
def rename_repo def rename_repo
path_was = previous_changes['path'].first path_was = previous_changes['path'].first
old_path_with_namespace = File.join(namespace_dir, path_was) old_path_with_namespace = File.join(namespace.full_path, path_was)
new_path_with_namespace = File.join(namespace_dir, path) new_path_with_namespace = File.join(namespace.full_path, path)
Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
......
...@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService ...@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'This service sends notifications about projects events to Mattermost channels.<br /> 'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service: To set up this service:
<ol> <ol>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li>
<li>Paste the webhook <strong>URL</strong> into the field bellow. </li> <li>Paste the webhook <strong>URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The channel and username are optional. </li> <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
</ol>' </ol>'
end end
...@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService ...@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def default_channel_placeholder def default_channel_placeholder
"town-square" "Channel handle (e.g. town-square)"
end end
end end
...@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService ...@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help def help
'This service sends notifications about projects events to Slack channels.<br /> 'This service sends notifications about projects events to Slack channels.<br />
To setup this service: To set up this service:
<ol> <ol>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below. </li> <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The channel and username are optional. </li> <li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
</ol>' </ol>'
end end
...@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService ...@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def default_channel_placeholder def default_channel_placeholder
"#general" "Channel name (e.g. general)"
end end
end end
...@@ -116,9 +116,7 @@ class Repository ...@@ -116,9 +116,7 @@ class Repository
offset: offset, offset: offset,
after: after, after: after,
before: before, before: before,
# --follow doesn't play well with --skip. See: follow: path.present?,
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
follow: false,
skip_merges: skip_merges skip_merges: skip_merges
} }
......
...@@ -364,7 +364,7 @@ class User < ActiveRecord::Base ...@@ -364,7 +364,7 @@ class User < ActiveRecord::Base
def reference_pattern def reference_pattern
%r{ %r{
#{Regexp.escape(reference_prefix)} #{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}) (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
}x }x
end end
......
class CreateBranchService < BaseService class CreateBranchService < BaseService
def execute(branch_name, ref) def execute(branch_name, ref)
create_master_branch if project.empty_repo?
result = ValidateNewBranchService.new(project, current_user) result = ValidateNewBranchService.new(project, current_user)
.execute(branch_name) .execute(branch_name)
...@@ -19,4 +21,16 @@ class CreateBranchService < BaseService ...@@ -19,4 +21,16 @@ class CreateBranchService < BaseService
def success(branch) def success(branch)
super().merge(branch: branch) super().merge(branch: branch)
end end
private
def create_master_branch
project.repository.commit_file(
current_user,
'/README.md',
'',
message: 'Add README.md',
branch_name: 'master',
update: false)
end
end end
...@@ -18,7 +18,8 @@ module Groups ...@@ -18,7 +18,8 @@ module Groups
end end
group.children.each do |group| group.children.each do |group|
DestroyService.new(group, current_user).async_execute # This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute
end end
group.really_destroy! group.really_destroy!
......
...@@ -6,6 +6,8 @@ module MergeRequests ...@@ -6,6 +6,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI # Executed when you do merge via GitLab UI
# #
class MergeService < MergeRequests::BaseService class MergeService < MergeRequests::BaseService
MergeError = Class.new(StandardError)
attr_reader :merge_request, :source attr_reader :merge_request, :source
def execute(merge_request) def execute(merge_request)
...@@ -20,11 +22,7 @@ module MergeRequests ...@@ -20,11 +22,7 @@ module MergeRequests
return log_merge_error('Merge request is not mergeable', save_message_on_model: true) return log_merge_error('Merge request is not mergeable', save_message_on_model: true)
end end
if @merge_request.target_project.above_size_limit? check_size_limit
message = Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error
return log_merge_error(message, save_message_on_model: true)
end
@source = find_merge_source @source = find_merge_source
...@@ -38,6 +36,8 @@ module MergeRequests ...@@ -38,6 +36,8 @@ module MergeRequests
success success
end end
end end
rescue MergeError => e
log_merge_error(e.message, save_message_on_model: true)
end end
def hooks_validation_pass?(merge_request) def hooks_validation_pass?(merge_request)
...@@ -74,19 +74,13 @@ module MergeRequests ...@@ -74,19 +74,13 @@ module MergeRequests
commit_id = repository.merge(current_user, source, merge_request, options) commit_id = repository.merge(current_user, source, merge_request, options)
if commit_id raise MergeError, 'Conflicts detected during merge' unless commit_id
merge_request.update(merge_commit_sha: commit_id)
else merge_request.update(merge_commit_sha: commit_id)
log_merge_error('Conflicts detected during merge', save_message_on_model: true)
false
end
rescue GitHooksService::PreReceiveError => e rescue GitHooksService::PreReceiveError => e
log_merge_error(e.message, save_message_on_model: true) raise MergeError, e.message
false
rescue StandardError => e rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") raise MergeError, "Something went wrong during merge: #{e.message}"
log_merge_error(e.message)
false
ensure ensure
merge_request.update(in_progress_merge_commit_sha: nil) merge_request.update(in_progress_merge_commit_sha: nil)
end end
...@@ -114,17 +108,24 @@ module MergeRequests ...@@ -114,17 +108,24 @@ module MergeRequests
merge_request.to_reference(full: true) merge_request.to_reference(full: true)
end end
def check_size_limit
if @merge_request.target_project.above_size_limit?
message = Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error
raise MergeError, message
end
end
def find_merge_source def find_merge_source
return merge_request.diff_head_sha unless merge_request.squash return merge_request.diff_head_sha unless merge_request.squash
squash_result = SquashService.new(project, current_user, params).execute(merge_request) squash_result = SquashService.new(project, current_user, params).execute(merge_request)
if squash_result[:status] == :success case squash_result[:status]
when :success
squash_result[:squash_sha] squash_result[:squash_sha]
else when :error
log_merge_error("Squashing #{merge_request_info} failed") raise MergeError, squash_result[:message]
nil
end end
end end
end end
......
...@@ -18,8 +18,7 @@ module MergeRequests ...@@ -18,8 +18,7 @@ module MergeRequests
end end
if merge_request.squash_in_progress? if merge_request.squash_in_progress?
log_error('Squash task canceled: Another squash is already in progress') return error('Squash task canceled: another squash is already in progress')
return false
end end
run_git_command( run_git_command(
......
...@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader ...@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader
File.join(self.class.artifacts_cache_path, @build.artifacts_path) File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end end
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
def filename def filename
file.try(:filename) file.try(:filename)
end end
......
...@@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader ...@@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader
storage :file storage :file
def store_dir def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end end
end end
...@@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader ...@@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader
storage :file storage :file
def store_dir def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end end
def exists? def exists?
......
...@@ -4,15 +4,12 @@ class FileUploader < GitlabUploader ...@@ -4,15 +4,12 @@ class FileUploader < GitlabUploader
storage :file storage :file
attr_accessor :project, :secret attr_accessor :project
attr_reader :secret
def initialize(project, secret = nil) def initialize(project, secret = nil)
@project = project @project = project
@secret = secret || self.class.generate_secret @secret = secret || generate_secret
end
def base_dir
"uploads"
end end
def store_dir def store_dir
...@@ -23,10 +20,6 @@ class FileUploader < GitlabUploader ...@@ -23,10 +20,6 @@ class FileUploader < GitlabUploader
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end end
def secure_url
File.join("/uploads", @secret, file.filename)
end
def to_markdown def to_markdown
to_h[:markdown] to_h[:markdown]
end end
...@@ -35,17 +28,23 @@ class FileUploader < GitlabUploader ...@@ -35,17 +28,23 @@ class FileUploader < GitlabUploader
filename = image_or_video? ? self.file.basename : self.file.filename filename = image_or_video? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]") escaped_filename = filename.gsub("]", "\\]")
markdown = "[#{escaped_filename}](#{self.secure_url})" markdown = "[#{escaped_filename}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous? markdown.prepend("!") if image_or_video? || dangerous?
{ {
alt: filename, alt: filename,
url: self.secure_url, url: secure_url,
markdown: markdown markdown: markdown
} }
end end
def self.generate_secret private
def generate_secret
SecureRandom.hex SecureRandom.hex
end end
def secure_url
File.join('/uploads', @secret, file.filename)
end
end end
class GitlabUploader < CarrierWave::Uploader::Base class GitlabUploader < CarrierWave::Uploader::Base
def self.base_dir
'uploads'
end
delegate :base_dir, to: :class
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
# Reduce disk IO # Reduce disk IO
def move_to_cache def move_to_cache
true true
......
...@@ -27,6 +27,8 @@ module UploaderHelper ...@@ -27,6 +27,8 @@ module UploaderHelper
extension_match?(DANGEROUS_EXT) extension_match?(DANGEROUS_EXT)
end end
private
def extension_match?(extensions) def extension_match?(extensions)
return false unless file return false unless file
...@@ -40,8 +42,4 @@ module UploaderHelper ...@@ -40,8 +42,4 @@ module UploaderHelper
extensions.include?(extension.downcase) extensions.include?(extension.downcase)
end end
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
end end
# DurationValidator
#
# Validate the format conforms with ChronicDuration
#
# Example:
#
# class ApplicationSetting < ActiveRecord::Base
# validates :default_artifacts_expire_in, presence: true, duration: true
# end
#
class DurationValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError
record.errors.add(attribute, "is not a correct duration")
end
end
...@@ -232,8 +232,16 @@ ...@@ -232,8 +232,16 @@
.col-sm-10 .col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control' = f.number_field :max_artifacts_size, class: 'form-control'
.help-block .help-block
Set the maximum file size each jobs's artifacts can have Set the maximum file size for each job's artifacts
= link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
.form-group
= f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :default_artifacts_expire_in, class: 'form-control'
.help-block
Set the default expiration time for each job's artifacts.
0 for unlimited.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
- if Gitlab.config.registry.enabled - if Gitlab.config.registry.enabled
%fieldset %fieldset
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
= runner.short_sha = runner.short_sha
%td %td
= runner.description = runner.description
%td
= runner.version
%td %td
- if runner.shared? - if runner.shared?
n/a n/a
......
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
%th Type %th Type
%th Runner token %th Runner token
%th Description %th Description
%th Version
%th Projects %th Projects
%th Jobs %th Jobs
%th Tags %th Tags
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
- page_title "Projects" - page_title "Projects"
- header_title "Projects", dashboard_projects_path - header_title "Projects", dashboard_projects_path
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
- if @projects.any? || params[:filter_projects] - if @projects.any? || params[:filter_projects]
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required." = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group .username.form-group
= f.label :username = f.label :username
= f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken. %p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available. %p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability... %p.validation-pending.hide Checking username availability...
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -101,5 +101,3 @@ ...@@ -101,5 +101,3 @@
$("#created-personal-access-token").click(function() { $("#created-personal-access-token").click(function() {
this.select(); this.select();
}); });
$("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block.activity-filter-block .nav-block.activity-filter-block
- if current_user - if current_user
.controls .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
= icon('rss') = icon('rss')
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
= line_content = line_content
- when :parallel - when :parallel
%td.old_line.diff-line-num{ data: { linenumber: line_old } } %td.old_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_old), "##{line_old}" %a{ href: "##{line_old}", data: { linenumber: line_old } }
= line_content = line_content
%td.new_line.diff-line-num{ data: { linenumber: line_new } } %td.new_line.diff-line-num{ data: { linenumber: line_new } }
= link_to raw(line_new), "##{line_new}" %a{ href: "##{line_new}", data: { linenumber: line_new } }
= line_content = line_content
- if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
= render "projects/issues/head" = render "projects/issues/head"
......
%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
":index" => "index",
":data-issue-id" => "issue.id",
"@mousedown" => "mouseDown",
"@mousemove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
%issue-card-inner{ ":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath" }
...@@ -12,12 +12,16 @@ ...@@ -12,12 +12,16 @@
.form-group .form-group
= label_tag :branch_name, nil, class: 'control-label' = label_tag :branch_name, nil, class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
.help-block.text-danger.js-branch-name-error .help-block.text-danger.js-branch-name-error
.form-group .form-group
= label_tag :ref, 'Create from', class: 'control-label' = label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' = hidden_field_tag :ref, params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } })
.help-block Existing branch name, tag, or commit SHA .help-block Existing branch name, tag, or commit SHA
.form-actions .form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
......
.content-block.build-header .content-block.build-header.top-area
.header-content .header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
Job Job
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
- if @build.user - if @build.user
= render "user" = render "user"
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable? .nav-controls
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post - if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left') = icon('angle-double-left')
- status = pipeline.status
- show_commit = local_assigns.fetch(:show_commit, true)
- show_branch = local_assigns.fetch(:show_branch, true)
%tr.commit
%td.commit-link
= render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span.pipeline-id ##{pipeline.id}
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
- else
%span.api.monospace API
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
- if pipeline.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
%td.branch-commit
- if pipeline.ref && show_branch
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- if show_commit
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
= link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
%td
= render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
- artifacts = pipeline.builds.latest.with_artifacts_not_expired
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
.btn-group.inline
- if actions.any?
.btn-group
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= build.name
- if artifacts.present?
.btn-group
%button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
= link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do
= icon("download")
%span Download '#{build.name}' artifacts
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
= icon("remove")
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
- if diff_file.renamed_file - if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } } %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path = old_path
&rarr; &rarr;
%strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } } %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path = new_path
- else - else
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment