summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-08-19 13:01:58 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-08-19 13:01:58 +0300
commit6db65143db5003f74ddb1c9868a3d852e5661a0a (patch)
tree07a792396398c16d78f8f3ed87926237aeb68271 /app/assets
parent74461ccc1f1503c86102b7d8e790ebac0d28fc0b (diff)
parent12fe6a6fd733110acc72aa0f5bdaec2b1fa1f358 (diff)
downloadgitlab-ce-6db65143db5003f74ddb1c9868a3d852e5661a0a.tar.gz
Merge branch 'master' into dz-merge-request-version
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/abuse_reports.js.es639
-rw-r--r--app/assets/javascripts/api.js51
-rw-r--r--app/assets/javascripts/application.js13
-rw-r--r--app/assets/javascripts/awards_handler.js54
-rw-r--r--app/assets/javascripts/blob/template_selector.js22
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es657
-rw-r--r--app/assets/javascripts/boards/components/board.js.es685
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es649
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es643
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es619
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es689
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es654
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es625
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es644
-rw-r--r--app/assets/javascripts/boards/models/label.js.es69
-rw-r--r--app/assets/javascripts/boards/models/list.js.es6125
-rw-r--r--app/assets/javascripts/boards/models/user.js.es68
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es661
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es6112
-rwxr-xr-xapp/assets/javascripts/boards/test_utils/simulate_drag.js119
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/build.js35
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js1
-rw-r--r--app/assets/javascripts/create_label.js.es6126
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es649
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6188
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es6107
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es618
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es660
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es635
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es635
-rw-r--r--app/assets/javascripts/diff_notes/mixins/namespace.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es687
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es688
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es653
-rw-r--r--app/assets/javascripts/dispatcher.js7
-rw-r--r--app/assets/javascripts/files_comment_button.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6 (renamed from app/assets/javascripts/gfm_auto_complete.js)65
-rw-r--r--app/assets/javascripts/labels_select.js88
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/javascripts/merge_conflict_data_provider.js.es6341
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es685
-rw-r--r--app/assets/javascripts/merge_request.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js33
-rw-r--r--app/assets/javascripts/merge_request_widget.js4
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/notes.js142
-rw-r--r--app/assets/javascripts/pipeline.js.es615
-rw-r--r--app/assets/javascripts/protected_branch_create.js.es64
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es610
-rw-r--r--app/assets/javascripts/single_file_diff.js16
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es651
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es629
-rw-r--r--app/assets/javascripts/users_select.js9
-rw-r--r--app/assets/stylesheets/behaviors.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss8
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss7
-rw-r--r--app/assets/stylesheets/framework/highlight.scss15
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss5
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss7
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss7
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss7
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss13
-rw-r--r--app/assets/stylesheets/highlight/white.scss13
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss5
-rw-r--r--app/assets/stylesheets/pages/admin.scss42
-rw-r--r--app/assets/stylesheets/pages/boards.scss329
-rw-r--r--app/assets/stylesheets/pages/builds.scss88
-rw-r--r--app/assets/stylesheets/pages/issuable.scss9
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss238
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss26
-rw-r--r--app/assets/stylesheets/pages/notes.scss77
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss193
-rw-r--r--app/assets/stylesheets/pages/profile.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss47
81 files changed, 3860 insertions, 211 deletions
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
new file mode 100644
index 00000000000..748084b0307
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -0,0 +1,39 @@
+window.gl = window.gl || {};
+((global) => {
+ const MAX_MESSAGE_LENGTH = 500;
+ const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
+
+ class AbuseReports {
+ constructor() {
+ $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
+ $(document)
+ .off('click', MESSAGE_CELL_SELECTOR)
+ .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
+ }
+
+ truncateLongMessage() {
+ const $messageCellElement = $(this);
+ const reportMessage = $messageCellElement.text();
+ if (reportMessage.length > MAX_MESSAGE_LENGTH) {
+ $messageCellElement.data('original-message', reportMessage);
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ }
+ }
+
+ toggleMessageTruncation() {
+ const $messageCellElement = $(this);
+ const originalMessage = $messageCellElement.data('original-message');
+ if (!originalMessage) return;
+ if ($messageCellElement.data('message-truncated') === 'true') {
+ $messageCellElement.data('message-truncated', 'false');
+ $messageCellElement.text(originalMessage);
+ } else {
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+ }
+ }
+ }
+
+ global.AbuseReports = AbuseReports;
+})(window.gl);
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 49c2ac0dac3..84b292e59c6 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -9,10 +9,11 @@
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+ issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+
group: function(group_id, callback) {
- var url;
- url = Api.buildUrl(Api.groupPath);
- url = url.replace(':id', group_id);
+ var url = Api.buildUrl(Api.groupPath)
+ .replace(':id', group_id);
return $.ajax({
url: url,
data: {
@@ -24,8 +25,7 @@
});
},
groups: function(query, skip_ldap, callback) {
- var url;
- url = Api.buildUrl(Api.groupsPath);
+ var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
@@ -39,8 +39,7 @@
});
},
namespaces: function(query, callback) {
- var url;
- url = Api.buildUrl(Api.namespacesPath);
+ var url = Api.buildUrl(Api.namespacesPath);
return $.ajax({
url: url,
data: {
@@ -54,8 +53,7 @@
});
},
projects: function(query, order, callback) {
- var url;
- url = Api.buildUrl(Api.projectsPath);
+ var url = Api.buildUrl(Api.projectsPath);
return $.ajax({
url: url,
data: {
@@ -70,9 +68,8 @@
});
},
newLabel: function(project_id, data, callback) {
- var url;
- url = Api.buildUrl(Api.labelsPath);
- url = url.replace(':id', project_id);
+ var url = Api.buildUrl(Api.labelsPath)
+ .replace(':id', project_id);
data.private_token = gon.api_token;
return $.ajax({
url: url,
@@ -86,9 +83,8 @@
});
},
groupProjects: function(group_id, query, callback) {
- var url;
- url = Api.buildUrl(Api.groupProjectsPath);
- url = url.replace(':id', group_id);
+ var url = Api.buildUrl(Api.groupProjectsPath)
+ .replace(':id', group_id);
return $.ajax({
url: url,
data: {
@@ -102,8 +98,8 @@
});
},
licenseText: function(key, data, callback) {
- var url;
- url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ var url = Api.buildUrl(Api.licensePath)
+ .replace(':key', key);
return $.ajax({
url: url,
data: data
@@ -112,19 +108,32 @@
});
},
gitignoreText: function(key, callback) {
- var url;
- url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ var url = Api.buildUrl(Api.gitignorePath)
+ .replace(':key', key);
return $.get(url, function(gitignore) {
return callback(gitignore);
});
},
gitlabCiYml: function(key, callback) {
- var url;
- url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ var url = Api.buildUrl(Api.gitlabCiYmlPath)
+ .replace(':key', key);
return $.get(url, function(file) {
return callback(file);
});
},
+ issueTemplate: function(namespacePath, projectPath, key, type, callback) {
+ var url = Api.buildUrl(Api.issuableTemplatePath)
+ .replace(':key', key)
+ .replace(':type', type)
+ .replace(':project_path', projectPath)
+ .replace(':namespace_path', namespacePath);
+ $.ajax({
+ url: url,
+ dataType: 'json'
+ }).done(function(file) {
+ callback(null, file);
+ }).error(callback);
+ },
buildUrl: function(url) {
if (gon.relative_url_root != null) {
url = gon.relative_url_root + url;
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index f1aab067351..a122fa2d637 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -26,7 +26,7 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
-/*= require ace/ace */
+/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
@@ -41,6 +41,7 @@
/*= require date.format */
/*= require_directory ./behaviors */
/*= require_directory ./blob */
+/*= require_directory ./templates */
/*= require_directory ./commit */
/*= require_directory ./extensions */
/*= require_directory ./lib/utils */
@@ -223,8 +224,14 @@
return $('.navbar-toggle').toggleClass('active');
});
$body.on("click", ".js-toggle-diff-comments", function(e) {
- $(this).toggleClass('active');
- $(this).closest(".diff-file").find(".notes_holder").toggle();
+ var $this = $(this);
+ $this.toggleClass('active');
+ var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+ if ($this.hasClass('active')) {
+ notesHolders.show();
+ } else {
+ notesHolders.hide();
+ }
return e.preventDefault();
});
$document.off("click", '.js-confirm-danger');
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 2c5b83e4f1e..aee1c29eee3 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,5 +1,6 @@
(function() {
this.AwardsHandler = (function() {
+ const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
@@ -130,7 +131,7 @@
counter = $emojiButton.find('.js-counter');
counter.text(parseInt(counter.text()) + 1);
$emojiButton.addClass('active');
- this.addMeToUserList(votesBlock, emoji);
+ this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton);
}
} else {
@@ -176,11 +177,11 @@
counterNumber = parseInt(counter.text(), 10);
if (counterNumber > 1) {
counter.text(counterNumber - 1);
- this.removeMeFromUserList($emojiButton, emoji);
+ this.removeYouFromUserList($emojiButton, emoji);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('destroy');
counter.text('0');
- this.removeMeFromUserList($emojiButton, emoji);
+ this.removeYouFromUserList($emojiButton, emoji);
if ($emojiButton.parents('.note').length) {
this.removeEmoji($emojiButton);
}
@@ -204,43 +205,48 @@
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
};
- AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) {
+ AwardsHandler.prototype.toSentence = function(list) {
+ if(list.length <= 2){
+ return list.join(' and ');
+ }
+ else{
+ return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
+ }
+ };
+
+ AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
var authors, awardBlock, newAuthors, originalTitle;
awardBlock = $emojiButton;
originalTitle = this.getAwardTooltip(awardBlock);
- authors = originalTitle.split(', ');
- authors.splice(authors.indexOf('me'), 1);
- newAuthors = authors.join(', ');
- awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors);
- return this.resetTooltip(awardBlock);
+ authors = originalTitle.split(FROM_SENTENCE_REGEX);
+ authors.splice(authors.indexOf('You'), 1);
+ return awardBlock
+ .closest('.js-emoji-btn')
+ .removeData('title')
+ .removeAttr('data-title')
+ .removeAttr('data-original-title')
+ .attr('title', this.toSentence(authors))
+ .tooltip('fixTitle');
};
- AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) {
+ AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
var awardBlock, origTitle, users;
awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
origTitle = this.getAwardTooltip(awardBlock);
users = [];
if (origTitle) {
- users = origTitle.trim().split(', ');
+ users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
- users.push('me');
- awardBlock.attr('title', users.join(', '));
- return this.resetTooltip(awardBlock);
- };
-
- AwardsHandler.prototype.resetTooltip = function(award) {
- var cb;
- award.tooltip('destroy');
- cb = function() {
- return award.tooltip();
- };
- return setTimeout(cb, 200);
+ users.unshift('You');
+ return awardBlock
+ .attr('title', this.toSentence(users))
+ .tooltip('fixTitle');
};
AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
var $emojiButton, buttonHtml, emojiCssClass;
emojiCssClass = this.resolveNameToCssClass(emoji);
- buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
+ buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
$emojiButton = $(buttonHtml);
$emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
this.animateEmoji($emojiButton);
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 2cf0a6631b8..b0a37ef0e0a 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -9,6 +9,7 @@
}
this.onClick = bind(this.onClick, this);
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
@@ -60,11 +61,26 @@
return this.requestFile(item);
};
- TemplateSelector.prototype.requestFile = function(item) {};
+ TemplateSelector.prototype.requestFile = function(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ };
- TemplateSelector.prototype.requestFileSuccess = function(file) {
+ TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
- return this.editor.focus();
+ if (!skipFocus) this.editor.focus();
+ };
+
+ TemplateSelector.prototype.startLoadingSpinner = function() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ };
+
+ TemplateSelector.prototype.stopLoadingSpinner = function() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
};
return TemplateSelector;
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
new file mode 100644
index 00000000000..2c65d4427be
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -0,0 +1,57 @@
+//= require vue
+//= require vue-resource
+//= require Sortable
+//= require_tree ./models
+//= require_tree ./stores
+//= require_tree ./services
+//= require_tree ./mixins
+//= require ./components/board
+//= require ./components/new_list_dropdown
+//= require ./vue_resource_interceptor
+
+$(() => {
+ const $boardApp = document.getElementById('board-app'),
+ Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+
+ if (gl.IssueBoardsApp) {
+ gl.IssueBoardsApp.$destroy(true);
+ }
+
+ gl.IssueBoardsApp = new Vue({
+ el: $boardApp,
+ components: {
+ 'board': gl.issueBoards.Board
+ },
+ data: {
+ state: Store.state,
+ loading: true,
+ endpoint: $boardApp.dataset.endpoint,
+ disabled: $boardApp.dataset.disabled === 'true',
+ issueLinkBase: $boardApp.dataset.issueLinkBase
+ },
+ init: Store.create.bind(Store),
+ created () {
+ gl.boardService = new BoardService(this.endpoint);
+ },
+ ready () {
+ Store.disabled = this.disabled;
+ gl.boardService.all()
+ .then((resp) => {
+ resp.json().forEach((board) => {
+ const list = Store.addList(board);
+
+ if (list.type === 'done') {
+ list.position = Infinity;
+ } else if (list.type === 'backlog') {
+ list.position = -1;
+ }
+ });
+
+ Store.addBlankState();
+ this.loading = false;
+ });
+ }
+ });
+});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
new file mode 100644
index 00000000000..e17784e7948
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -0,0 +1,85 @@
+//= require ./board_blank_state
+//= require ./board_delete
+//= require ./board_list
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.Board = Vue.extend({
+ components: {
+ 'board-list': gl.issueBoards.BoardList,
+ 'board-delete': gl.issueBoards.BoardDelete,
+ 'board-blank-state': gl.issueBoards.BoardBlankState
+ },
+ props: {
+ list: Object,
+ disabled: Boolean,
+ issueLinkBase: String
+ },
+ data () {
+ return {
+ query: '',
+ filters: Store.state.filters
+ };
+ },
+ watch: {
+ query () {
+ this.list.filters = this.getFilterData();
+ this.list.getIssues(true);
+ },
+ filters: {
+ handler () {
+ this.list.page = 1;
+ this.list.getIssues(true);
+ },
+ deep: true
+ }
+ },
+ methods: {
+ getFilterData () {
+ const filters = this.filters;
+ let queryData = { search: this.query };
+
+ Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
+
+ return queryData;
+ }
+ },
+ ready () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ disabled: this.disabled,
+ group: 'boards',
+ draggable: '.is-draggable',
+ handle: '.js-board-handle',
+ onEnd: (e) => {
+ document.body.classList.remove('is-dragging');
+
+ if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+ const order = this.sortable.toArray(),
+ $board = this.$parent.$refs.board[e.oldIndex + 1],
+ list = $board.list;
+
+ $board.$destroy(true);
+
+ this.$nextTick(() => {
+ Store.state.lists.splice(e.newIndex, 0, list);
+ Store.moveList(list, order);
+ });
+ }
+ }
+ });
+
+ if (bp.getBreakpointSize() === 'xs') {
+ options.handle = '.js-board-drag-handle';
+ }
+
+ this.sortable = Sortable.create(this.$el.parentNode, options);
+ },
+ beforeDestroy () {
+ Store.state.lists.$remove(this.list);
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
new file mode 100644
index 00000000000..63d72d857d9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -0,0 +1,49 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardBlankState = Vue.extend({
+ data () {
+ return {
+ predefinedLabels: [
+ new ListLabel({ title: 'Development', color: '#5CB85C' }),
+ new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Production', color: '#FF5F00' }),
+ new ListLabel({ title: 'Ready', color: '#FF0000' })
+ ]
+ }
+ },
+ methods: {
+ addDefaultLists () {
+ this.clearBlankState();
+
+ this.predefinedLabels.forEach((label, i) => {
+ Store.addList({
+ title: label.title,
+ position: i,
+ list_type: 'label',
+ label: {
+ title: label.title,
+ color: label.color
+ }
+ });
+ });
+
+ // Save the labels
+ gl.boardService.generateDefaultLists()
+ .then((resp) => {
+ resp.json().forEach((listObj) => {
+ const list = Store.findList('title', listObj.title);
+
+ list.id = listObj.id;
+ list.label.id = listObj.label.id;
+ list.getIssues();
+ });
+ });
+ },
+ clearBlankState: Store.removeBlankState.bind(Store)
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
new file mode 100644
index 00000000000..4a7cfeaeab2
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -0,0 +1,43 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardCard = Vue.extend({
+ props: {
+ list: Object,
+ issue: Object,
+ issueLinkBase: String,
+ disabled: Boolean,
+ index: Number
+ },
+ methods: {
+ filterByLabel (label, e) {
+ let labelToggleText = label.title;
+ const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
+ $(e.target).tooltip('hide');
+
+ if (labelIndex === -1) {
+ Store.state.filters['label_name'].push(label.title);
+ $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ } else {
+ Store.state.filters['label_name'].splice(labelIndex, 1);
+ labelToggleText = Store.state.filters['label_name'][0];
+ $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ }
+
+ const selectedLabels = Store.state.filters['label_name'];
+ if (selectedLabels.length === 0) {
+ labelToggleText = 'Label';
+ } else if (selectedLabels.length > 1) {
+ labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
+ }
+
+ $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+
+ Store.updateFiltersUrl();
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
new file mode 100644
index 00000000000..34653cd48ef
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -0,0 +1,19 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardDelete = Vue.extend({
+ props: {
+ list: Object
+ },
+ methods: {
+ deleteBoard () {
+ $(this.$el).tooltip('hide');
+
+ if (confirm('Are you sure you want to delete this list?')) {
+ this.list.destroy();
+ }
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
new file mode 100644
index 00000000000..1503d14c508
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -0,0 +1,89 @@
+//= require ./board_card
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardList = Vue.extend({
+ components: {
+ 'board-card': gl.issueBoards.BoardCard
+ },
+ props: {
+ disabled: Boolean,
+ list: Object,
+ issues: Array,
+ loading: Boolean,
+ issueLinkBase: String
+ },
+ data () {
+ return {
+ scrollOffset: 250,
+ filters: Store.state.filters
+ };
+ },
+ watch: {
+ filters: {
+ handler () {
+ this.list.loadingMore = false;
+ this.$els.list.scrollTop = 0;
+ },
+ deep: true
+ }
+ },
+ methods: {
+ listHeight () {
+ return this.$els.list.getBoundingClientRect().height;
+ },
+ scrollHeight () {
+ return this.$els.list.scrollHeight;
+ },
+ scrollTop () {
+ return this.$els.list.scrollTop + this.listHeight();
+ },
+ loadNextPage () {
+ const getIssues = this.list.nextPage();
+
+ if (getIssues) {
+ this.list.loadingMore = true;
+ getIssues.then(() => {
+ this.list.loadingMore = false;
+ });
+ }
+ },
+ },
+ ready () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ group: 'issues',
+ sort: false,
+ disabled: this.disabled,
+ onStart: (e) => {
+ const card = this.$refs.issue[e.oldIndex];
+
+ Store.moving.issue = card.issue;
+ Store.moving.list = card.list;
+ },
+ onAdd: (e) => {
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+ },
+ onRemove: (e) => {
+ this.$refs.issue[e.oldIndex].$destroy(true);
+ }
+ });
+
+ if (bp.getBreakpointSize() === 'xs') {
+ options.handle = '.js-card-drag-handle';
+ }
+
+ this.sortable = Sortable.create(this.$els.list, options);
+
+ // Scroll event on list to load more
+ this.$els.list.onscroll = () => {
+ if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+ this.loadNextPage();
+ }
+ };
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
new file mode 100644
index 00000000000..1a4d8157970
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -0,0 +1,54 @@
+$(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ $('.js-new-board-list').each(function () {
+ const $this = $(this);
+
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+
+ $this.glDropdown({
+ data(term, callback) {
+ $.get($this.attr('data-labels'))
+ .then((resp) => {
+ callback(resp);
+ });
+ },
+ renderRow (label) {
+ const active = Store.findList('title', label.title),
+ $li = $('<li />'),
+ $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ }),
+ $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
+ });
+
+ return $li.append($a.prepend($labelColor));
+ },
+ search: {
+ fields: ['title']
+ },
+ filterable: true,
+ selectable: true,
+ clicked (label, $el, e) {
+ e.preventDefault();
+
+ if (!Store.findList('title', label.title)) {
+ Store.new({
+ title: label.title,
+ position: Store.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color
+ }
+ });
+ }
+ }
+ });
+ });
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
new file mode 100644
index 00000000000..b7afe4897b6
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -0,0 +1,25 @@
+((w) => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
+ let defaultSortOptions = {
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ fallbackOnBody: true,
+ ghostClass: 'is-ghost',
+ filter: '.has-tooltip',
+ scrollSensitivity: 100,
+ scrollSpeed: 20,
+ onStart () {
+ document.body.classList.add('is-dragging');
+ },
+ onEnd () {
+ document.body.classList.remove('is-dragging');
+ }
+ }
+
+ Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ return defaultSortOptions;
+ };
+})(window);
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
new file mode 100644
index 00000000000..eb082103de9
--- /dev/null
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -0,0 +1,44 @@
+class ListIssue {
+ constructor (obj) {
+ this.id = obj.iid;
+ this.title = obj.title;
+ this.confidential = obj.confidential;
+ this.labels = [];
+
+ if (obj.assignee) {
+ this.assignee = new ListUser(obj.assignee);
+ }
+
+ obj.labels.forEach((label) => {
+ this.labels.push(new ListLabel(label));
+ });
+
+ this.priority = this.labels.reduce((max, label) => {
+ return (label.priority < max) ? label.priority : max;
+ }, Infinity);
+ }
+
+ addLabel (label) {
+ if (!this.findLabel(label)) {
+ this.labels.push(new ListLabel(label));
+ }
+ }
+
+ findLabel (findLabel) {
+ return this.labels.filter( label => label.title === findLabel.title )[0];
+ }
+
+ removeLabel (removeLabel) {
+ if (removeLabel) {
+ this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+ }
+ }
+
+ removeLabels (labels) {
+ labels.forEach(this.removeLabel.bind(this));
+ }
+
+ getLists () {
+ return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+ }
+}
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
new file mode 100644
index 00000000000..e81e91fe972
--- /dev/null
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -0,0 +1,9 @@
+class ListLabel {
+ constructor (obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ this.color = obj.color;
+ this.description = obj.description;
+ this.priority = (obj.priority !== null) ? obj.priority : Infinity;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
new file mode 100644
index 00000000000..be2b8c568a8
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -0,0 +1,125 @@
+class List {
+ constructor (obj) {
+ this.id = obj.id;
+ this._uid = this.guid();
+ this.position = obj.position;
+ this.title = obj.title;
+ this.type = obj.list_type;
+ this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+ this.filters = gl.issueBoards.BoardsStore.state.filters;
+ this.page = 1;
+ this.loading = true;
+ this.loadingMore = false;
+ this.issues = [];
+
+ if (obj.label) {
+ this.label = new ListLabel(obj.label);
+ }
+
+ if (this.type !== 'blank' && this.id) {
+ this.getIssues();
+ }
+ }
+
+ guid() {
+ const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
+ }
+
+ save () {
+ return gl.boardService.createList(this.label.id)
+ .then((resp) => {
+ const data = resp.json();
+
+ this.id = data.id;
+ this.type = data.list_type;
+ this.position = data.position;
+
+ return this.getIssues();
+ });
+ }
+
+ destroy () {
+ gl.issueBoards.BoardsStore.state.lists.$remove(this);
+ gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
+
+ gl.boardService.destroyList(this.id);
+ }
+
+ update () {
+ gl.boardService.updateList(this.id, this.position);
+ }
+
+ nextPage () {
+ if (Math.floor(this.issues.length / 20) === this.page) {
+ this.page++;
+
+ return this.getIssues(false);
+ }
+ }
+
+ canSearch () {
+ return this.type === 'backlog';
+ }
+
+ getIssues (emptyIssues = true) {
+ const filters = this.filters;
+ let data = { page: this.page };
+
+ Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
+
+ if (this.label) {
+ data.label_name = data.label_name.filter( label => label !== this.label.title );
+ }
+
+ if (emptyIssues) {
+ this.loading = true;
+ }
+
+ return gl.boardService.getIssuesForList(this.id, data)
+ .then((resp) => {
+ const data = resp.json();
+ this.loading = false;
+
+ if (emptyIssues) {
+ this.issues = [];
+ }
+
+ this.createIssues(data);
+ });
+ }
+
+ createIssues (data) {
+ data.forEach((issueObj) => {
+ this.addIssue(new ListIssue(issueObj));
+ });
+ }
+
+ addIssue (issue, listFrom) {
+ this.issues.push(issue);
+
+ if (this.label) {
+ issue.addLabel(this.label);
+ }
+
+ if (listFrom) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
+ }
+ }
+
+ findIssue (id) {
+ return this.issues.filter( issue => issue.id === id )[0];
+ }
+
+ removeIssue (removeIssue) {
+ this.issues = this.issues.filter((issue) => {
+ const matchesRemove = removeIssue.id === issue.id;
+
+ if (matchesRemove) {
+ issue.removeLabel(this.label);
+ }
+
+ return !matchesRemove;
+ });
+ }
+}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
new file mode 100644
index 00000000000..904b3a68507
--- /dev/null
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -0,0 +1,8 @@
+class ListUser {
+ constructor (user) {
+ this.id = user.id;
+ this.name = user.name;
+ this.username = user.username;
+ this.avatar = user.avatar_url;
+ }
+}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
new file mode 100644
index 00000000000..9b80fb2e99f
--- /dev/null
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -0,0 +1,61 @@
+class BoardService {
+ constructor (root) {
+ Vue.http.options.root = root;
+
+ this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
+ generate: {
+ method: 'POST',
+ url: `${root}/lists/generate.json`
+ }
+ });
+ this.issue = Vue.resource(`${root}/issues{/id}`, {});
+ this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
+
+ Vue.http.interceptors.push((request, next) => {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ next();
+ });
+ }
+
+ all () {
+ return this.lists.get();
+ }
+
+ generateDefaultLists () {
+ return this.lists.generate({});
+ }
+
+ createList (label_id) {
+ return this.lists.save({}, {
+ list: {
+ label_id
+ }
+ });
+ }
+
+ updateList (id, position) {
+ return this.lists.update({ id }, {
+ list: {
+ position
+ }
+ });
+ }
+
+ destroyList (id) {
+ return this.lists.delete({ id });
+ }
+
+ getIssuesForList (id, filter = {}) {
+ let data = { id };
+ Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+
+ return this.issues.get(data);
+ }
+
+ moveIssue (id, from_list_id, to_list_id) {
+ return this.issue.update({ id }, {
+ from_list_id,
+ to_list_id
+ });
+ }
+};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
new file mode 100644
index 00000000000..18f26a1f911
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -0,0 +1,112 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardsStore = {
+ disabled: false,
+ state: {},
+ moving: {
+ issue: {},
+ list: {}
+ },
+ create () {
+ this.state.lists = [];
+ this.state.filters = {
+ author_id: gl.utils.getParameterValues('author_id')[0],
+ assignee_id: gl.utils.getParameterValues('assignee_id')[0],
+ milestone_title: gl.utils.getParameterValues('milestone_title')[0],
+ label_name: gl.utils.getParameterValues('label_name[]')
+ };
+ },
+ addList (listObj) {
+ const list = new List(listObj);
+ this.state.lists.push(list);
+
+ return list;
+ },
+ new (listObj) {
+ const list = this.addList(listObj),
+ backlogList = this.findList('type', 'backlog', 'backlog');
+
+ list
+ .save()
+ .then(() => {
+ // Remove any new issues from the backlog
+ // as they will be visible in the new list
+ list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+ });
+ this.removeBlankState();
+ },
+ updateNewListDropdown (listId) {
+ $(`.js-board-list-${listId}`).removeClass('is-active');
+ },
+ shouldAddBlankState () {
+ // Decide whether to add the blank state
+ return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+ },
+ addBlankState () {
+ if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
+
+ this.addList({
+ id: 'blank',
+ list_type: 'blank',
+ title: 'Welcome to your Issue Board!',
+ position: 0
+ });
+ },
+ removeBlankState () {
+ this.removeList('blank');
+
+ $.cookie('issue_board_welcome_hidden', 'true', {
+ expires: 365 * 10
+ });
+ },
+ welcomeIsHidden () {
+ return $.cookie('issue_board_welcome_hidden') === 'true';
+ },
+ removeList (id, type = 'blank') {
+ const list = this.findList('id', id, type);
+
+ if (!list) return;
+
+ this.state.lists = this.state.lists.filter( list => list.id !== id );
+ },
+ moveList (listFrom, orderLists) {
+ orderLists.forEach((id, i) => {
+ const list = this.findList('id', parseInt(id));
+
+ list.position = i;
+ });
+ listFrom.update();
+ },
+ moveIssueToList (listFrom, listTo, issue) {
+ const issueTo = listTo.findIssue(issue.id),
+ issueLists = issue.getLists(),
+ listLabels = issueLists.map( listIssue => listIssue.label );
+
+ // Add to new lists issues if it doesn't already exist
+ if (!issueTo) {
+ listTo.addIssue(issue, listFrom);
+ }
+
+ if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+ issueLists.forEach((list) => {
+ list.removeIssue(issue);
+ })
+ issue.removeLabels(listLabels);
+ } else {
+ listFrom.removeIssue(issue);
+ }
+ },
+ findList (key, val, type = 'label') {
+ return this.state.lists.filter((list) => {
+ const byType = type ? list['type'] === type : true;
+
+ return list[key] === val && byType;
+ })[0];
+ },
+ updateFiltersUrl () {
+ history.pushState(null, null, `?${$.param(this.state.filters)}`);
+ }
+ };
+})();
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
new file mode 100755
index 00000000000..75f8b730195
--- /dev/null
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -0,0 +1,119 @@
+(function () {
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ function getTraget(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1]
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ var fromEl = getTraget(options.from);
+ var toEl = getTraget(options.to);
+ var scrollable = options.scrollable;
+
+ var fromRect = getRect(fromEl);
+ var toRect = getRect(toEl);
+
+ var startTime = new Date().getTime();
+ var duration = options.duration || 1000;
+ simulateEvent(fromEl, 'mousedown', {button: 0});
+ options.ontap && options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ var dragInterval = setInterval(function loop() {
+ var progress = (new Date().getTime() - startTime) / duration;
+ var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
+})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..f9f9f7999d4
--- /dev/null
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -0,0 +1,10 @@
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ Vue.nextTick(() => {
+ setTimeout(() => {
+ Vue.activeResources--;
+ }, 500);
+ });
+ next();
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 3d9b824d406..0d7d29bb0d0 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -6,19 +6,26 @@
Build.state = null;
- function Build(page_url, build_url, build_status, state1) {
- this.page_url = page_url;
- this.build_url = build_url;
- this.build_status = build_status;
- this.state = state1;
+ function Build(options) {
+ this.page_url = options.page_url;
+ this.build_url = options.build_url;
+ this.build_status = options.build_status;
+ this.state = options.state1;
+ this.build_stage = options.build_stage;
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
+ this.updateDropdown = bind(this.updateDropdown, this);
clearInterval(Build.interval);
this.bp = Breakpoints.get();
- this.hideSidebar();
$('.js-build-sidebar').niceScroll();
+
+ this.populateJobs(this.build_stage);
+ this.updateStageDropdownText(this.build_stage);
+ this.hideSidebar();
+
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
$(window).off('resize.build').on('resize.build', this.hideSidebar);
+ $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
@@ -132,6 +139,22 @@
}
};
+ Build.prototype.populateJobs = function(stage) {
+ $('.build-job').hide();
+ $('.build-job[data-stage="' + stage + '"]').show();
+ };
+
+ Build.prototype.updateStageDropdownText = function(stage) {
+ $('.stage-selection').text(stage);
+ };
+
+ Build.prototype.updateDropdown = function(e) {
+ e.preventDefault();
+ var stage = e.currentTarget.text;
+ this.updateStageDropdownText(stage);
+ this.populateJobs(stage);
+ };
+
return Build;
})();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index c82798cc6a5..c43af17442b 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -34,6 +34,7 @@
$(function() {
var clipboard;
+
clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError);
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
new file mode 100644
index 00000000000..46d1c3f00c1
--- /dev/null
+++ b/app/assets/javascripts/create_label.js.es6
@@ -0,0 +1,126 @@
+(function (w) {
+ class CreateLabelDropdown {
+ constructor ($el, projectId) {
+ this.$el = $el;
+ this.projectId = projectId;
+ this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
+ this.$cancelButton = $('.js-cancel-label-btn', this.$el);
+ this.$newLabelField = $('#new_label_name', this.$el);
+ this.$newColorField = $('#new_label_color', this.$el);
+ this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+ this.$newLabelError = $('.js-label-error', this.$el);
+ this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
+ this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
+
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.disable();
+
+ this.cleanBinding();
+ this.addBinding();
+ }
+
+ cleanBinding () {
+ this.$colorSuggestions.off('click');
+ this.$newLabelField.off('keyup change');
+ this.$newColorField.off('keyup change');
+ this.$dropdownBack.off('click');
+ this.$cancelButton.off('click');
+ this.$newLabelCreateButton.off('click');
+ }
+
+ addBinding () {
+ const self = this;
+
+ this.$colorSuggestions.on('click', function (e) {
+ const $this = $(this);
+ self.addColorValue(e, $this);
+ });
+
+ this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
+ this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
+
+ this.$dropdownBack.on('click', this.resetForm.bind(this));
+
+ this.$cancelButton.on('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ self.resetForm();
+ self.$dropdownBack.trigger('click');
+ });
+
+ this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
+ }
+
+ addColorValue (e, $this) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$newColorField.val($this.data('color')).trigger('change');
+ this.$colorPreview
+ .css('background-color', $this.data('color'))
+ .parent()
+ .addClass('is-active');
+ }
+
+ enableLabelCreateButton () {
+ if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.enable();
+ } else {
+ this.$newLabelCreateButton.disable();
+ }
+ }
+
+ resetForm () {
+ this.$newLabelField
+ .val('')
+ .trigger('change');
+
+ this.$newColorField
+ .val('')
+ .trigger('change');
+
+ this.$colorPreview
+ .css('background-color', '')
+ .parent()
+ .removeClass('is-active');
+ }
+
+ saveLabel (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ Api.newLabel(this.projectId, {
+ name: this.$newLabelField.val(),
+ color: this.$newColorField.val()
+ }, (label) => {
+ this.$newLabelCreateButton.enable();
+
+ if (label.message) {
+ let errors;
+
+ if (typeof label.message === 'string') {
+ errors = label.message;
+ } else {
+ errors = label.message.map(function (value, key) {
+ return key + " " + value[0];
+ }).join("<br/>");
+ }
+
+ this.$newLabelError
+ .html(errors)
+ .show();
+ } else {
+ this.$dropdownBack.trigger('click');
+ }
+ });
+ }
+ }
+
+ if (!w.gl) {
+ w.gl = {};
+ }
+
+ gl.CreateLabelDropdown = CreateLabelDropdown;
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
new file mode 100644
index 00000000000..48bc7d77805
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -0,0 +1,49 @@
+((w) => {
+ w.CommentAndResolveBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ textareaIsEmpty: Boolean
+ },
+ computed: {
+ discussion: function () {
+ return CommentsStore.state[this.discussionId];
+ },
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ return this.discussion.isResolved();
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ if (this.textareaIsEmpty) {
+ return "Unresolve discussion";
+ } else {
+ return "Comment & unresolve discussion";
+ }
+ } else {
+ if (this.textareaIsEmpty) {
+ return "Resolve discussion";
+ } else {
+ return "Comment & resolve discussion";
+ }
+ }
+ }
+ },
+ ready: function () {
+ const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
+ this.textareaIsEmpty = $textarea.val() === '';
+
+ $textarea.on('input.comment-and-resolve-btn', () => {
+ this.textareaIsEmpty = $textarea.val() === '';
+ });
+ },
+ destroyed: function () {
+ $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
new file mode 100644
index 00000000000..ad80d1118df
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -0,0 +1,188 @@
+(() => {
+ JumpToDiscussion = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ discussionId: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ allResolved: function () {
+ return this.unresolvedDiscussionCount === 0;
+ },
+ showButton: function () {
+ if (this.discussionId) {
+ if (this.unresolvedDiscussionCount > 1) {
+ return true;
+ } else {
+ return this.discussionId !== this.lastResolvedId;
+ }
+ } else {
+ return this.unresolvedDiscussionCount >= 1;
+ }
+ },
+ lastResolvedId: function () {
+ let lastId;
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ lastId = discussion.id;
+ }
+ }
+ return lastId;
+ }
+ },
+ methods: {
+ jumpToNextUnresolvedDiscussion: function () {
+ let discussionsSelector,
+ discussionIdsInScope,
+ firstUnresolvedDiscussionId,
+ nextUnresolvedDiscussionId,
+ activeTab = window.mrTabs.currentAction,
+ hasDiscussionsToJumpTo = true,
+ jumpToFirstDiscussion = !this.discussionId;
+
+ const discussionIdsForElements = function(elements) {
+ return elements.map(function() {
+ return $(this).attr('data-discussion-id');
+ }).toArray();
+ };
+
+ const discussions = this.discussions;
+
+ if (activeTab === 'diffs') {
+ discussionsSelector = '.diffs .notes[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+ let unresolvedDiscussionCount = 0;
+
+ for (let i = 0; i < discussionIdsInScope.length; i++) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+ if (discussion && !discussion.isResolved()) {
+ unresolvedDiscussionCount++;
+ }
+ }
+
+ if (this.discussionId && !this.discussion.isResolved()) {
+ // If this is the last unresolved discussion on the diffs tab,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 1) {
+ hasDiscussionsToJumpTo = false;
+ }
+ } else {
+ // If there are no unresolved discussions on the diffs tab at all,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 0) {
+ hasDiscussionsToJumpTo = false;
+ }
+ }
+ } else if (activeTab !== 'notes') {
+ // If we are on the commits or builds tabs,
+ // there are no discussions to jump to.
+ hasDiscussionsToJumpTo = false;
+ }
+
+ if (!hasDiscussionsToJumpTo) {
+ // If there are no discussions to jump to on the current page,
+ // switch to the notes tab and jump to the first disucssion there.
+ window.mrTabs.activateTab('notes');
+ activeTab = 'notes';
+ jumpToFirstDiscussion = true;
+ }
+
+ if (activeTab === 'notes') {
+ discussionsSelector = '.discussion[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+ }
+
+ let currentDiscussionFound = false;
+ for (let i = 0; i < discussionIdsInScope.length; i++) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+
+ if (!discussion) {
+ // Discussions for comments on commits in this MR don't have a resolved status.
+ continue;
+ }
+
+ if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+ firstUnresolvedDiscussionId = discussionId;
+
+ if (jumpToFirstDiscussion) {
+ break;
+ }
+ }
+
+ if (!jumpToFirstDiscussion) {
+ if (currentDiscussionFound) {
+ if (!discussion.isResolved()) {
+ nextUnresolvedDiscussionId = discussionId;
+ break;
+ }
+ else {
+ continue;
+ }
+ }
+
+ if (discussionId === this.discussionId) {
+ currentDiscussionFound = true;
+ }
+ }
+ }
+
+ nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+
+ if (!nextUnresolvedDiscussionId) {
+ return;
+ }
+
+ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+
+ if (activeTab === 'notes') {
+ $target = $target.closest('.note-discussion');
+
+ // If the next discussion is closed, toggle it open.
+ if ($target.find('.js-toggle-content').is(':hidden')) {
+ $target.find('.js-toggle-button i').trigger('click')
+ }
+ } else if (activeTab === 'diffs') {
+ // Resolved discussions are hidden in the diffs tab by default.
+ // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+ // When jumping between unresolved discussions on the diffs tab, we show them.
+ $target.closest(".content").show();
+
+ $target = $target.closest("tr.notes_holder");
+ $target.show();
+
+ // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+ // 4 diff lines above it: the line the discussion was in response to + 3 context
+ let prevEl;
+ for (let i = 0; i < 4; i++) {
+ prevEl = $target.prev();
+
+ // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+ if (!prevEl.hasClass("line_holder")) {
+ break;
+ }
+
+ $target = prevEl;
+ }
+ }
+
+ $.scrollTo($target, {
+ offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ });
+ }
+ }
+ });
+
+ Vue.component('jump-to-discussion', JumpToDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
new file mode 100644
index 00000000000..be6ebc77947
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -0,0 +1,107 @@
+((w) => {
+ w.ResolveBtn = Vue.extend({
+ mixins: [
+ ButtonMixins
+ ],
+ props: {
+ noteId: Number,
+ discussionId: String,
+ resolved: Boolean,
+ namespacePath: String,
+ projectPath: String,
+ canResolve: Boolean,
+ resolvedBy: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ loading: false
+ };
+ },
+ watch: {
+ 'discussions': {
+ handler: 'updateTooltip',
+ deep: true
+ }
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ note: function () {
+ if (this.discussion) {
+ return this.discussion.getNote(this.noteId);
+ } else {
+ return undefined;
+ }
+ },
+ buttonText: function () {
+ if (this.isResolved) {
+ return `Resolved by ${this.resolvedByName}`;
+ } else if (this.canResolve) {
+ return 'Mark as resolved';
+ } else {
+ return 'Unable to resolve';
+ }
+ },
+ isResolved: function () {
+ if (this.note) {
+ return this.note.resolved;
+ } else {
+ return false;
+ }
+ },
+ resolvedByName: function () {
+ return this.note.resolved_by;
+ },
+ },
+ methods: {
+ updateTooltip: function () {
+ $(this.$els.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ },
+ resolve: function () {
+ if (!this.canResolve) return;
+
+ let promise;
+ this.loading = true;
+
+ if (this.isResolved) {
+ promise = ResolveService
+ .unresolve(this.namespace, this.noteId);
+ } else {
+ promise = ResolveService
+ .resolve(this.namespace, this.noteId);
+ }
+
+ promise.then((response) => {
+ this.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ this.discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
+ }
+
+ this.$nextTick(this.updateTooltip);
+ });
+ }
+ },
+ compiled: function () {
+ $(this.$els.button).tooltip({
+ container: 'body'
+ });
+ },
+ beforeDestroy: function () {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created: function () {
+ CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
new file mode 100644
index 00000000000..9e383b14a3e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -0,0 +1,18 @@
+((w) => {
+ w.ResolveCount = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ loggedOut: Boolean
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.resolvedDiscussionCount === this.discussionCount;
+ }
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
new file mode 100644
index 00000000000..e373b06b1eb
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -0,0 +1,60 @@
+((w) => {
+ w.ResolveDiscussionBtn = Vue.extend({
+ mixins: [
+ ButtonMixins
+ ],
+ props: {
+ discussionId: String,
+ mergeRequestId: Number,
+ namespacePath: String,
+ projectPath: String,
+ canResolve: Boolean,
+ },
+ data: function() {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ if (this.discussion) {
+ return this.discussion.isResolved();
+ } else {
+ return false;
+ }
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ return "Unresolve discussion";
+ } else {
+ return "Resolve discussion";
+ }
+ },
+ loading: function () {
+ if (this.discussion) {
+ return this.discussion.loading;
+ } else {
+ return false;
+ }
+ }
+ },
+ methods: {
+ resolve: function () {
+ ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
+ }
+ },
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
new file mode 100644
index 00000000000..22d9cf6c857
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -0,0 +1,35 @@
+//= require vue
+//= require vue-resource
+//= require_directory ./models
+//= require_directory ./stores
+//= require_directory ./services
+//= require_directory ./mixins
+//= require_directory ./components
+
+$(() => {
+ window.DiffNotesApp = new Vue({
+ el: '#diff-notes-app',
+ components: {
+ 'resolve-btn': ResolveBtn,
+ 'resolve-discussion-btn': ResolveDiscussionBtn,
+ 'comment-and-resolve-btn': CommentAndResolveBtn
+ },
+ methods: {
+ compileComponents: function () {
+ const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
+ if ($components.length) {
+ $components.each(function () {
+ DiffNotesApp.$compile($(this).get(0));
+ });
+ }
+ }
+ }
+ });
+
+ new Vue({
+ el: '#resolve-count-app',
+ components: {
+ 'resolve-count': ResolveCount
+ }
+ });
+});
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
new file mode 100644
index 00000000000..a05f885201d
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -0,0 +1,35 @@
+((w) => {
+ w.DiscussionMixins = {
+ computed: {
+ discussionCount: function () {
+ return Object.keys(this.discussions).length;
+ },
+ resolvedDiscussionCount: function () {
+ let resolvedCount = 0;
+
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (discussion.isResolved()) {
+ resolvedCount++;
+ }
+ }
+
+ return resolvedCount;
+ },
+ unresolvedDiscussionCount: function () {
+ let unresolvedCount = 0;
+
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ unresolvedCount++;
+ }
+ }
+
+ return unresolvedCount;
+ }
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
new file mode 100644
index 00000000000..d278678085b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
@@ -0,0 +1,9 @@
+((w) => {
+ w.ButtonMixins = {
+ computed: {
+ namespace: function () {
+ return `${this.namespacePath}/${this.projectPath}`;
+ }
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
new file mode 100644
index 00000000000..488714e4870
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -0,0 +1,87 @@
+class DiscussionModel {
+ constructor (discussionId) {
+ this.id = discussionId;
+ this.notes = {};
+ this.loading = false;
+ this.canResolve = false;
+ }
+
+ createNote (noteId, canResolve, resolved, resolved_by) {
+ Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
+ }
+
+ deleteNote (noteId) {
+ Vue.delete(this.notes, noteId);
+ }
+
+ getNote (noteId) {
+ return this.notes[noteId];
+ }
+
+ notesCount() {
+ return Object.keys(this.notes).length;
+ }
+
+ isResolved () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ resolveAllNotes (resolved_by) {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ note.resolved = true;
+ note.resolved_by = resolved_by;
+ }
+ }
+ }
+
+ unResolveAllNotes () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.resolved) {
+ note.resolved = false;
+ note.resolved_by = null;
+ }
+ }
+ }
+
+ updateHeadline (data) {
+ const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`);
+
+ if (data.discussion_headline_html) {
+ if ($discussionHeadline.length) {
+ $discussionHeadline.replaceWith(data.discussion_headline_html);
+ } else {
+ $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html);
+ }
+ } else {
+ $discussionHeadline.remove();
+ }
+ }
+
+ isResolvable () {
+ if (!this.canResolve) {
+ return false;
+ }
+
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.canResolve) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
new file mode 100644
index 00000000000..f2d2d389c38
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -0,0 +1,9 @@
+class NoteModel {
+ constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+ this.discussionId = discussionId;
+ this.id = noteId;
+ this.canResolve = canResolve;
+ this.resolved = resolved;
+ this.resolved_by = resolved_by;
+ }
+}
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
new file mode 100644
index 00000000000..de771ff814b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -0,0 +1,88 @@
+((w) => {
+ class ResolveServiceClass {
+ constructor() {
+ this.noteResource = Vue.resource('notes{/noteId}/resolve');
+ this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
+ }
+
+ setCSRF() {
+ Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+
+ prepareRequest(namespace) {
+ this.setCSRF();
+ Vue.http.options.root = `/${namespace}`;
+ }
+
+ resolve(namespace, noteId) {
+ this.prepareRequest(namespace);
+
+ return this.noteResource.save({ noteId }, {});
+ }
+
+ unresolve(namespace, noteId) {
+ this.prepareRequest(namespace);
+
+ return this.noteResource.delete({ noteId }, {});
+ }
+
+ toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId],
+ isResolved = discussion.isResolved();
+ let promise;
+
+ if (isResolved) {
+ promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+ } else {
+ promise = this.resolveAll(namespace, mergeRequestId, discussionId);
+ }
+
+ promise.then((response) => {
+ discussion.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ if (isResolved) {
+ discussion.unResolveAllNotes();
+ } else {
+ discussion.resolveAllNotes(resolved_by);
+ }
+
+ discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+ }
+ })
+ }
+
+ resolveAll(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ this.prepareRequest(namespace);
+
+ discussion.loading = true;
+
+ return this.discussionResource.save({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+
+ unResolveAll(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ this.prepareRequest(namespace);
+
+ discussion.loading = true;
+
+ return this.discussionResource.delete({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+ }
+
+ w.ResolveService = new ResolveServiceClass();
+})(window);
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
new file mode 100644
index 00000000000..69522e1dac5
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -0,0 +1,53 @@
+((w) => {
+ w.CommentsStore = {
+ state: {},
+ get: function (discussionId, noteId) {
+ return this.state[discussionId].getNote(noteId);
+ },
+ createDiscussion: function (discussionId, canResolve) {
+ let discussion = this.state[discussionId];
+ if (!this.state[discussionId]) {
+ discussion = new DiscussionModel(discussionId);
+ Vue.set(this.state, discussionId, discussion);
+ }
+
+ if (canResolve !== undefined) {
+ discussion.canResolve = canResolve;
+ }
+
+ return discussion;
+ },
+ create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
+ const discussion = this.createDiscussion(discussionId);
+
+ discussion.createNote(noteId, canResolve, resolved, resolved_by);
+ },
+ update: function (discussionId, noteId, resolved, resolved_by) {
+ const discussion = this.state[discussionId];
+ const note = discussion.getNote(noteId);
+ note.resolved = resolved;
+ note.resolved_by = resolved_by;
+ },
+ delete: function (discussionId, noteId) {
+ const discussion = this.state[discussionId];
+ discussion.deleteNote(noteId);
+
+ if (discussion.notesCount() === 0) {
+ Vue.delete(this.state, discussionId);
+ }
+ },
+ unresolvedDiscussionIds: function () {
+ let ids = [];
+
+ for (const discussionId in this.state) {
+ const discussion = this.state[discussionId];
+
+ if (!discussion.isResolved()) {
+ ids.push(discussion.id);
+ }
+ }
+
+ return ids;
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 3946e861976..74c4ab563f9 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -55,6 +55,7 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
+ new IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
@@ -62,6 +63,7 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
+ new IssuableTemplateSelectors();
break;
case 'projects:tags:new':
new ZenMode();
@@ -86,6 +88,8 @@
new ZenMode();
new MergedButtons();
break;
+ case "projects:merge_requests:conflicts":
+ window.mcui = new MergeConflictResolver()
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
@@ -192,6 +196,9 @@
case 'edit':
new Labels();
}
+ case 'abuse_reports':
+ new gl.AbuseReports();
+ break;
}
break;
case 'dashboard':
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 09b5eb398d4..b2e49b71fec 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -33,7 +33,7 @@
this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val();
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
- $(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+ $(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);
}
FilesCommentButton.prototype.render = function(e) {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js.es6
index 2e5b15f4b77..3dca06d36b1 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -223,7 +223,7 @@
}
}
});
- return this.input.atwho({
+ this.input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
@@ -249,6 +249,68 @@
}
}
});
+ // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+ this.input.filter('[data-supports-slash-commands="true"]').atwho({
+ at: '/',
+ alias: 'commands',
+ searchKey: 'search',
+ displayTpl: function(value) {
+ var tpl = '<li>/${name}';
+ if (value.aliases.length > 0) {
+ tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ }
+ if (value.params.length > 0) {
+ tpl += ' <small><%- params.join(" ") %></small>';
+ }
+ if (value.description !== '') {
+ tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ tpl += '</li>';
+ return _.template(tpl)(value);
+ },
+ insertTpl: function(value) {
+ var tpl = "/${name} ";
+ var reference_prefix = null;
+ if (value.params.length > 0) {
+ reference_prefix = value.params[0][0];
+ if (/^[@%~]/.test(reference_prefix)) {
+ tpl += '<%- reference_prefix %>';
+ }
+ }
+ return _.template(tpl)({ reference_prefix: reference_prefix });
+ },
+ suffix: '',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(commands) {
+ return $.map(commands, function(c) {
+ var search = c.name;
+ if (c.aliases.length > 0) {
+ search = search + " " + c.aliases.join(" ");
+ }
+ return {
+ name: c.name,
+ aliases: c.aliases,
+ params: c.params,
+ description: c.description,
+ search: search
+ };
+ });
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
+ var match = regexp.exec(subtext);
+ if (match) {
+ return match[1];
+ } else {
+ return null;
+ }
+ }
+ }
+ });
+ return;
},
destroyAtWho: function() {
return this.input.atwho('destroy');
@@ -265,6 +327,7 @@
this.input.atwho('load', 'mergerequests', data.mergerequests);
this.input.atwho('load', ':', data.emojis);
this.input.atwho('load', '~', data.labels);
+ this.input.atwho('load', '/', data.commands);
return $(':focus').trigger('keyup');
}
};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 675dd5b7cea..0526430989f 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
@@ -13,8 +13,6 @@
if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
- newLabelField = $('#new_label_name');
- newColorField = $('#new_label_color');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
defaultLabel = $dropdown.data('default-label');
@@ -24,10 +22,6 @@
$form = $dropdown.closest('form');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$value = $block.find('.value');
- $newLabelError = $('.js-label-error');
- $colorPreview = $('.js-dropdown-label-color-preview');
- $newLabelCreateButton = $('.js-new-label-btn');
- $newLabelError.hide();
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
@@ -36,60 +30,9 @@
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
- if (newLabelField.length) {
- $('.suggest-colors-dropdown a').on("click", function(e) {
- e.preventDefault();
- e.stopPropagation();
- newColorField.val($(this).data('color')).trigger('change');
- return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active');
- });
- resetForm = function() {
- newLabelField.val('').trigger('change');
- newColorField.val('').trigger('change');
- return $colorPreview.css('background-color', '').parent().removeClass('is-active');
- };
- $('.dropdown-menu-back').on('click', function() {
- return resetForm();
- });
- $('.js-cancel-label-btn').on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- resetForm();
- return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
- });
- enableLabelCreateButton = function() {
- if (newLabelField.val() !== '' && newColorField.val() !== '') {
- $newLabelError.hide();
- return $newLabelCreateButton.enable();
- } else {
- return $newLabelCreateButton.disable();
- }
- };
- saveLabel = function() {
- return Api.newLabel(projectId, {
- name: newLabelField.val(),
- color: newColorField.val()
- }, function(label) {
- var errors;
- $newLabelCreateButton.enable();
- if (label.message != null) {
- errors = _.map(label.message, function(value, key) {
- return key + " " + value[0];
- });
- return $newLabelError.html(errors.join("<br/>")).show();
- } else {
- return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
- }
- });
- };
- newLabelField.on('keyup change', enableLabelCreateButton);
- newColorField.on('keyup change', enableLabelCreateButton);
- $newLabelCreateButton.disable().on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- return saveLabel();
- });
- }
+
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+
saveLabelData = function() {
var data, selected;
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
@@ -270,6 +213,9 @@
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
$value.removeAttr('style');
+ if (page === 'projects:boards:show') {
+ return;
+ }
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
@@ -289,7 +235,7 @@
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
- clicked: function(label) {
+ clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
if ($dropdown.hasClass('js-filter-bulk-update')) {
@@ -298,7 +244,23 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (page === 'projects:boards:show') {
+ if (label.isAny) {
+ gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+ } else if (label.title) {
+ gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+ } else {
+ var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+ filters = filters.filter(function (label) {
+ return label !== $el.text().trim();
+ });
+ gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+ }
+
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ return;
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 130479642f3..b6636de5767 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -104,9 +104,12 @@
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
- return gl.text.removeListeners = function(form) {
+ gl.text.removeListeners = function(form) {
return $('.js-md', form).off();
};
+ return gl.text.truncate = function(string, maxLength) {
+ return string.substr(0, (maxLength - 3)) + '...';
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
new file mode 100644
index 00000000000..cd92df8ddc5
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6
@@ -0,0 +1,341 @@
+const HEAD_HEADER_TEXT = 'HEAD//our changes';
+const ORIGIN_HEADER_TEXT = 'origin//their changes';
+const HEAD_BUTTON_TITLE = 'Use ours';
+const ORIGIN_BUTTON_TITLE = 'Use theirs';
+
+
+class MergeConflictDataProvider {
+
+ getInitialData() {
+ const diffViewType = $.cookie('diff_view');
+
+ return {
+ isLoading : true,
+ hasError : false,
+ isParallel : diffViewType === 'parallel',
+ diffViewType : diffViewType,
+ isSubmitting : false,
+ conflictsData : {},
+ resolutionData : {}
+ }
+ }
+
+
+ decorateData(vueInstance, data) {
+ this.vueInstance = vueInstance;
+
+ if (data.type === 'error') {
+ vueInstance.hasError = true;
+ data.errorMessage = data.message;
+ }
+ else {
+ data.shortCommitSha = data.commit_sha.slice(0, 7);
+ data.commitMessage = data.commit_message;
+
+ this.setParallelLines(data);
+ this.setInlineLines(data);
+ this.updateResolutionsData(data);
+ }
+
+ vueInstance.conflictsData = data;
+ vueInstance.isSubmitting = false;
+
+ const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
+ vueInstance.conflictsData.conflictsText = conflictsText;
+ }
+
+
+ updateResolutionsData(data) {
+ const vi = this.vueInstance;
+
+ data.files.forEach( (file) => {
+ file.sections.forEach( (section) => {
+ if (section.conflict) {
+ vi.$set(`resolutionData['${section.id}']`, false);
+ }
+ });
+ });
+ }
+
+
+ setParallelLines(data) {
+ data.files.forEach( (file) => {
+ file.filePath = this.getFilePath(file);
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+ file.parallelLines = [];
+ const linesObj = { left: [], right: [] };
+
+ file.sections.forEach( (section) => {
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ linesObj.left.push(this.getOriginHeaderLine(id));
+ linesObj.right.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach( (line) => {
+ const { type } = line;
+
+ if (conflict) {
+ if (type === 'old') {
+ linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
+ }
+ else if (type === 'new') {
+ linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
+ }
+ }
+ else {
+ const lineType = type || 'context';
+
+ linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
+ }
+ });
+
+ this.checkLineLengths(linesObj);
+ });
+
+ for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ file.parallelLines.push([
+ linesObj.right[i],
+ linesObj.left[i]
+ ]);
+ }
+
+ });
+ }
+
+
+ checkLineLengths(linesObj) {
+ let { left, right } = linesObj;
+
+ if (left.length !== right.length) {
+ if (left.length > right.length) {
+ const diff = left.length - right.length;
+ for (let i = 0; i < diff; i++) {
+ right.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ else {
+ const diff = right.length - left.length;
+ for (let i = 0; i < diff; i++) {
+ left.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ }
+ }
+
+
+ setInlineLines(data) {
+ data.files.forEach( (file) => {
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+ file.filePath = this.getFilePath(file);
+ file.inlineLines = []
+
+ file.sections.forEach( (section) => {
+ let currentLineType = 'new';
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ file.inlineLines.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach( (line) => {
+ const { type } = line;
+
+ if ((type === 'new' || type === 'old') && currentLineType !== type) {
+ currentLineType = type;
+ file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
+ }
+
+ this.decorateLineForInlineView(line, id, conflict);
+ file.inlineLines.push(line);
+ })
+
+ if (conflict) {
+ file.inlineLines.push(this.getOriginHeaderLine(id));
+ }
+ });
+ });
+ }
+
+
+ handleSelected(sectionId, selection) {
+ const vi = this.vueInstance;
+
+ vi.resolutionData[sectionId] = selection;
+ vi.conflictsData.files.forEach( (file) => {
+ file.inlineLines.forEach( (line) => {
+ if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
+ this.markLine(line, selection);
+ }
+ });
+
+ file.parallelLines.forEach( (lines) => {
+ const left = lines[0];
+ const right = lines[1];
+ const hasSameId = right.id === sectionId || left.id === sectionId;
+ const isLeftMatch = left.hasConflict || left.isHeader;
+ const isRightMatch = right.hasConflict || right.isHeader;
+
+ if (hasSameId && (isLeftMatch || isRightMatch)) {
+ this.markLine(left, selection);
+ this.markLine(right, selection);
+ }
+ })
+ });
+ }
+
+
+ updateViewType(newType) {
+ const vi = this.vueInstance;
+
+ if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
+ return;
+ }
+
+ vi.diffView = newType;
+ vi.isParallel = newType === 'parallel';
+ $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
+ $('.content-wrapper .container-fluid').toggleClass('container-limited');
+ }
+
+
+ markLine(line, selection) {
+ if (selection === 'head' && line.isHead) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ }
+ else if (selection === 'origin' && line.isOrigin) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ }
+ else {
+ line.isSelected = false;
+ line.isUnselected = true;
+ }
+ }
+
+
+ getConflictsCount() {
+ return Object.keys(this.vueInstance.resolutionData).length;
+ }
+
+
+ getResolvedCount() {
+ let count = 0;
+ const data = this.vueInstance.resolutionData;
+
+ for (const id in data) {
+ const resolution = data[id];
+ if (resolution) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+
+ isReadyToCommit() {
+ const { conflictsData, isSubmitting } = this.vueInstance
+ const allResolved = this.getConflictsCount() === this.getResolvedCount();
+ const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
+
+ return !isSubmitting && hasCommitMessage && allResolved;
+ }
+
+
+ getCommitButtonText() {
+ const initial = 'Commit conflict resolution';
+ const inProgress = 'Committing...';
+ const vue = this.vueInstance;
+
+ return vue ? vue.isSubmitting ? inProgress : initial : initial;
+ }
+
+
+ decorateLineForInlineView(line, id, conflict) {
+ const { type } = line;
+ line.id = id;
+ line.hasConflict = conflict;
+ line.isHead = type === 'new';
+ line.isOrigin = type === 'old';
+ line.hasMatch = type === 'match';
+ line.richText = line.rich_text;
+ line.isSelected = false;
+ line.isUnselected = false;
+ }
+
+ getLineForParallelView(line, id, lineType, isHead) {
+ const { old_line, new_line, rich_text } = line;
+ const hasConflict = lineType === 'conflict';
+
+ return {
+ id,
+ lineType,
+ hasConflict,
+ isHead : hasConflict && isHead,
+ isOrigin : hasConflict && !isHead,
+ hasMatch : lineType === 'match',
+ lineNumber : isHead ? new_line : old_line,
+ section : isHead ? 'head' : 'origin',
+ richText : rich_text,
+ isSelected : false,
+ isUnselected : false
+ }
+ }
+
+
+ getHeadHeaderLine(id) {
+ return {
+ id : id,
+ richText : HEAD_HEADER_TEXT,
+ buttonTitle : HEAD_BUTTON_TITLE,
+ type : 'new',
+ section : 'head',
+ isHeader : true,
+ isHead : true,
+ isSelected : false,
+ isUnselected: false
+ }
+ }
+
+
+ getOriginHeaderLine(id) {
+ return {
+ id : id,
+ richText : ORIGIN_HEADER_TEXT,
+ buttonTitle : ORIGIN_BUTTON_TITLE,
+ type : 'old',
+ section : 'origin',
+ isHeader : true,
+ isOrigin : true,
+ isSelected : false,
+ isUnselected: false
+ }
+ }
+
+
+ handleFailedRequest(vueInstance, data) {
+ vueInstance.hasError = true;
+ vueInstance.conflictsData.errorMessage = 'Something went wrong!';
+ }
+
+
+ getCommitData() {
+ return {
+ commit_message: this.vueInstance.conflictsData.commitMessage,
+ sections: this.vueInstance.resolutionData
+ }
+ }
+
+
+ getFilePath(file) {
+ const { old_path, new_path } = file;
+ return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
+ }
+
+}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
new file mode 100644
index 00000000000..77bffbcb403
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -0,0 +1,85 @@
+//= require vue
+
+class MergeConflictResolver {
+
+ constructor() {
+ this.dataProvider = new MergeConflictDataProvider()
+ this.initVue()
+ }
+
+
+ initVue() {
+ const that = this;
+ this.vue = new Vue({
+ el : '#conflicts',
+ name : 'MergeConflictResolver',
+ data : this.dataProvider.getInitialData(),
+ created : this.fetchData(),
+ computed : this.setComputedProperties(),
+ methods : {
+ handleSelected(sectionId, selection) {
+ that.dataProvider.handleSelected(sectionId, selection);
+ },
+ handleViewTypeChange(newType) {
+ that.dataProvider.updateViewType(newType);
+ },
+ commit() {
+ that.commit();
+ }
+ }
+ })
+ }
+
+
+ setComputedProperties() {
+ const dp = this.dataProvider;
+
+ return {
+ conflictsCount() { return dp.getConflictsCount() },
+ resolvedCount() { return dp.getResolvedCount() },
+ readyToCommit() { return dp.isReadyToCommit() },
+ commitButtonText() { return dp.getCommitButtonText() }
+ }
+ }
+
+
+ fetchData() {
+ const dp = this.dataProvider;
+
+ $.get($('#conflicts').data('conflictsPath'))
+ .done((data) => {
+ dp.decorateData(this.vue, data);
+ })
+ .error((data) => {
+ dp.handleFailedRequest(this.vue, data);
+ })
+ .always(() => {
+ this.vue.isLoading = false;
+
+ this.vue.$nextTick(() => {
+ $('#conflicts .js-syntax-highlight').syntaxHighlight();
+ });
+
+ if (this.vue.diffViewType === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
+ })
+ }
+
+
+ commit() {
+ this.vue.isSubmitting = true;
+
+ $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
+ .done((data) => {
+ window.location.href = data.redirect_to;
+ })
+ .error(() => {
+ new Flash('Something went wrong!');
+ })
+ .always(() => {
+ this.vue.isSubmitting = false;
+ });
+ }
+
+}
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 47e6dd1084d..56ebf84c4f6 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -34,7 +34,7 @@
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
- return new MergeRequestTabs(this.opts);
+ window.mrTabs = new MergeRequestTabs(this.opts);
} else {
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 52c2ed61012..ad08209d61e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -9,10 +9,13 @@
MergeRequestTabs.prototype.buildsLoaded = false;
+ MergeRequestTabs.prototype.pipelinesLoaded = false;
+
MergeRequestTabs.prototype.commitsLoaded = false;
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
+ this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
@@ -50,10 +53,15 @@
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
+ } else if (action === 'pipelines') {
+ this.loadPipelines($target.attr('href'));
+ this.expandView();
} else {
this.expandView();
}
- return this.setCurrentAction(action);
+ if (this.opts.setUrl) {
+ this.setCurrentAction(action);
+ }
};
MergeRequestTabs.prototype.scrollToElement = function(container) {
@@ -81,7 +89,8 @@
if (action === 'show') {
action = 'notes';
}
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '');
+ this.currentAction = action;
+ new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
if (action !== 'notes') {
new_state += "/" + action;
}
@@ -119,6 +128,11 @@
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
@@ -177,6 +191,21 @@
});
};
+ MergeRequestTabs.prototype.loadPipelines = function(source) {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ return this._get({
+ url: source + ".json",
+ success: function(data) {
+ $('#pipelines').html(data.html);
+ gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
+ this.pipelinesLoaded = true;
+ return this.scrollToElement("#pipelines");
+ }.bind(this)
+ });
+ };
+
MergeRequestTabs.prototype.toggleLoading = function(status) {
return $('.mr-loading-status .loading').toggle(status);
};
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 362aaa906d0..bd35b6f679d 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -28,7 +28,7 @@
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
- allowedPages = ['show', 'commits', 'builds', 'changes'];
+ allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
return $(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
@@ -53,7 +53,7 @@
return function(data) {
var callback, urlSuffix;
if (data.state === "merged") {
- urlSuffix = deleteSourceBranch ? '?delete_source=true' : '';
+ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index a0b65d20c03..e897ebdf630 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -94,7 +94,7 @@
$selectbox.hide();
return $value.css('display', '');
},
- clicked: function(selected) {
+ clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -102,7 +102,11 @@
if ($dropdown.hasClass('js-filter-bulk-update')) {
return;
}
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (page === 'projects:boards:show') {
+ gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
selectedMilestone = selected.name;
} else {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9ece474d994..d0d5cad813a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -68,6 +68,7 @@
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
$(document).on("click", ".js-comment-button", this.updateCloseButton);
$(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
$(document).on("click", ".js-note-delete", this.removeNote);
$(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
$(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
@@ -100,6 +101,7 @@
$(document).off("click", ".js-note-target-close");
$(document).off("click", ".js-note-discard");
$(document).off("keydown", ".js-note-text");
+ $(document).off('click', '.js-comment-resolve-button');
$('.note .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
@@ -201,7 +203,7 @@
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
will reset to @basePollingInterval.
-
+
Note: this function is used to gradually increase the polling interval
if there aren't new notes coming from the server
*/
@@ -223,7 +225,7 @@
/*
Render note in main comments area.
-
+
Note: for rendering inline notes use renderDiscussionNote
*/
@@ -231,7 +233,13 @@
var $notesList, votesBlock;
if (!note.valid) {
if (note.award) {
- new Flash('You have already awarded this emoji!', 'alert');
+ new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
+ }
+ else {
+ if (note.errors.commands_only) {
+ new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
+ this.refresh();
+ }
}
return;
}
@@ -245,6 +253,7 @@
$notesList.append(note.html).syntaxHighlight();
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.initTaskList();
+ this.refresh();
return this.updateNotesCount(1);
}
};
@@ -265,7 +274,7 @@
/*
Render note in discussion area.
-
+
Note: for rendering inline notes use renderDiscussionNote
*/
@@ -297,6 +306,11 @@
} else {
discussionContainer.append(note_html);
}
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+
gl.utils.localTimeAgo($('.js-timeago', note_html), false);
return this.updateNotesCount(1);
};
@@ -304,7 +318,7 @@
/*
Called in response the main target form has been successfully submitted.
-
+
Removes any errors.
Resets text and preview.
Resets buttons.
@@ -329,7 +343,7 @@
/*
Shows the main form and does some setup on it.
-
+
Sets some hidden fields in the form.
*/
@@ -343,13 +357,14 @@
form.find("#note_line_code").remove();
form.find("#note_position").remove();
form.find("#note_type").remove();
+ form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
return this.parentTimeline = form.parents('.timeline');
};
/*
General note form setup.
-
+
deactivates the submit button when text is empty
hides the preview button when text is empty
setup GFM auto complete
@@ -366,7 +381,7 @@
/*
Called in response to the new note form being submitted
-
+
Adds new note to list.
*/
@@ -381,19 +396,33 @@
/*
Called in response to the new note form being submitted
-
+
Adds new note to list.
*/
Notes.prototype.addDiscussionNote = function(xhr, note, status) {
+ var $form = $(xhr.target);
+
+ if ($form.attr('data-resolve-all') != null) {
+ var namespacePath = $form.attr('data-namespace-path'),
+ projectPath = $form.attr('data-project-path')
+ discussionId = $form.attr('data-discussion-id'),
+ mergeRequestId = $form.attr('data-noteable-iid'),
+ namespace = namespacePath + '/' + projectPath;
+
+ if (ResolveService != null) {
+ ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+ }
+ }
+
this.renderDiscussionNote(note);
- return this.removeDiscussionNoteForm($(xhr.target));
+ this.removeDiscussionNoteForm($form);
};
/*
Called in response to the edit note form being submitted
-
+
Updates the current note field.
*/
@@ -404,13 +433,18 @@
$html.syntaxHighlight();
$html.find('.js-task-list-container').taskList('enable');
$note_li = $('.note-row-' + note.id);
- return $note_li.replaceWith($html);
+
+ $note_li.replaceWith($html);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
};
/*
Called in response to clicking the edit note link
-
+
Replaces the note text with the note edit form
Adds a data attribute to the form with the original content of the note for cancellations
*/
@@ -450,7 +484,7 @@
/*
Called in response to clicking the edit note link
-
+
Hides edit form and restores the original note text to the editor textarea.
*/
@@ -472,7 +506,7 @@
/*
Called in response to deleting a note of any kind.
-
+
Removes the actual note from view.
Removes the whole discussion if the last note is being removed.
*/
@@ -485,6 +519,15 @@
var note, notes;
note = $(el);
notes = note.closest(".notes");
+
+ if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
+ ref = DiffNotesApp.$refs[noteId];
+
+ if (ref) {
+ ref.$destroy(true);
+ }
+ }
+
if (notes.find(".note").length === 1) {
notes.closest(".timeline-entry").remove();
notes.closest("tr").remove();
@@ -498,7 +541,7 @@
/*
Called in response to clicking the delete attachment link
-
+
Removes the attachment wrapper view, including image tag if it exists
Resets the note editing form
*/
@@ -515,7 +558,7 @@
/*
Called when clicking on the "reply" button for a diff line.
-
+
Shows the note form below the notes.
*/
@@ -523,17 +566,19 @@
var form, replyLink;
form = this.formClone.clone();
replyLink = $(e.target).closest(".js-discussion-reply-button");
- replyLink.hide();
- replyLink.after(form);
+ replyLink
+ .closest('.discussion-reply-holder')
+ .hide()
+ .after(form);
return this.setupDiscussionNoteForm(replyLink, form);
};
/*
Shows the diff or discussion form and does some setup on it.
-
+
Sets some hidden fields in the form.
-
+
Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
and "noteableId" data attributes set.
*/
@@ -549,15 +594,29 @@
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
+ form.find('.js-note-target-close').remove();
this.setupNoteForm(form);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ var $commentBtn = form.find('comment-and-resolve-btn');
+ $commentBtn
+ .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
+ DiffNotesApp.$compile($commentBtn.get(0));
+ }
+
form.find(".js-note-text").focus();
- return form.removeClass('js-main-target-form').addClass("discussion-form js-discussion-note-form");
+ form
+ .find('.js-comment-resolve-button')
+ .attr('data-discussion-id', dataHolder.data('discussionId'));
+ form
+ .removeClass('js-main-target-form')
+ .addClass("discussion-form js-discussion-note-form");
};
/*
Called when clicking on the "add a comment" button on the side of a diff line.
-
+
Inserts a temporary row for the form below the line.
Sets up the form and shows it.
*/
@@ -570,16 +629,19 @@
nextRow = row.next();
hasNotes = nextRow.is(".notes_holder");
addForm = false;
- targetContent = ".notes_content";
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>";
+ notesContentSelector = ".notes_content";
+ rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
if (this.isParallelView()) {
lineType = $link.data("lineType");
- targetContent += "." + lineType;
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>";
+ notesContentSelector += "." + lineType;
+ rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
}
+ notesContentSelector += " .content";
if (hasNotes) {
- notesContent = nextRow.find(targetContent);
+ nextRow.show();
+ notesContent = nextRow.find(notesContentSelector);
if (notesContent.length) {
+ notesContent.show();
replyButton = notesContent.find(".js-discussion-reply-button:visible");
if (replyButton.length) {
e.target = replyButton[0];
@@ -593,11 +655,13 @@
}
} else {
row.after(rowCssToAdd);
+ nextRow = row.next();
+ notesContent = nextRow.find(notesContentSelector);
addForm = true;
}
if (addForm) {
newForm = this.formClone.clone();
- newForm.appendTo(row.next().find(targetContent));
+ newForm.appendTo(notesContent);
return this.setupDiscussionNoteForm($link, newForm);
}
};
@@ -605,7 +669,7 @@
/*
Called in response to "cancel" on a diff note form.
-
+
Shows the reply button again.
Removes the form and if necessary it's temporary row.
*/
@@ -616,7 +680,9 @@
glForm = form.data('gl-form');
glForm.destroy();
form.find(".js-note-text").data("autosave").reset();
- form.prev(".js-discussion-reply-button").show();
+ form
+ .prev('.discussion-reply-holder')
+ .show();
if (row.is(".js-temp-notes-holder")) {
return row.remove();
} else {
@@ -634,7 +700,7 @@
/*
Called after an attachment file has been selected.
-
+
Updates the file name for the selected attachment.
*/
@@ -725,6 +791,18 @@
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
};
+ Notes.prototype.resolveDiscussion = function () {
+ var $this = $(this),
+ discussionId = $this.attr('data-discussion-id');
+
+ $this
+ .closest('form')
+ .attr('data-discussion-id', discussionId)
+ .attr('data-resolve-all', 'true')
+ .attr('data-namespace-path', $this.attr('data-namespace-path'))
+ .attr('data-project-path', $this.attr('data-project-path'));
+ };
+
return Notes;
})();
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
new file mode 100644
index 00000000000..bf33eb10100
--- /dev/null
+++ b/app/assets/javascripts/pipeline.js.es6
@@ -0,0 +1,15 @@
+(function() {
+ function toggleGraph() {
+ const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+ const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+ const $btnText = $(this).find('.toggle-btn-text');
+
+ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+ const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+
+ graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ }
+
+ $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
+})();
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6
index 00e20a03b04..2efca2414dc 100644
--- a/app/assets/javascripts/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branch_create.js.es6
@@ -44,8 +44,8 @@
// Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]');
- const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]');
+ const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
+ const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
this.$form.find('input[type="submit"]').removeAttr('disabled');
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index 8d42e268ebc..a59fcbfa082 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -39,12 +39,14 @@
_method: 'PATCH',
id: this.$wrap.data('banchId'),
protected_branch: {
- merge_access_level_attributes: {
+ merge_access_levels_attributes: [{
+ id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val()
- },
- push_access_level_attributes: {
+ }],
+ push_access_levels_attributes: [{
+ id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val()
- }
+ }]
}
},
success: () => {
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index b9ae497b0e5..156b9b8abec 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -35,10 +35,16 @@
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
- return this.collapsedContent.show();
+ this.collapsedContent.show();
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
} else if (this.content) {
this.collapsedContent.hide();
- return this.content.show();
+ this.content.show();
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
} else {
return this.getContentHTML();
}
@@ -57,7 +63,11 @@
_this.hasError = true;
_this.content = $(ERROR_HTML);
}
- return _this.collapsedContent.after(_this.content);
+ _this.collapsedContent.after(_this.content);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
};
})(this));
};
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
new file mode 100644
index 00000000000..c32ddf80219
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -0,0 +1,51 @@
+/*= require ../blob/template_selector */
+
+((global) => {
+ class IssuableTemplateSelector extends TemplateSelector {
+ constructor(...args) {
+ super(...args);
+ this.projectPath = this.dropdown.data('project-path');
+ this.namespacePath = this.dropdown.data('namespace-path');
+ this.issuableType = this.wrapper.data('issuable-type');
+ this.titleInput = $(`#${this.issuableType}_title`);
+
+ let initialQuery = {
+ name: this.dropdown.data('selected')
+ };
+
+ if (initialQuery.name) this.requestFile(initialQuery);
+
+ $('.reset-template', this.dropdown.parent()).on('click', () => {
+ if (this.currentTemplate) this.setInputValueToTemplateContent();
+ });
+ }
+
+ requestFile(query) {
+ this.startLoadingSpinner();
+ Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
+ this.currentTemplate = currentTemplate;
+ if (err) return; // Error handled by global AJAX error handler
+ this.stopLoadingSpinner();
+ this.setInputValueToTemplateContent();
+ });
+ return;
+ }
+
+ setInputValueToTemplateContent() {
+ // `this.requestFileSuccess` sets the value of the description input field
+ // to the content of the template selected.
+ if (this.titleInput.val() === '') {
+ // If the title has not yet been set, focus the title input and
+ // skip focusing the description input by setting `true` as the 2nd
+ // argument to `requestFileSuccess`.
+ this.requestFileSuccess(this.currentTemplate, true);
+ this.titleInput.focus();
+ } else {
+ this.requestFileSuccess(this.currentTemplate);
+ }
+ return;
+ }
+ }
+
+ global.IssuableTemplateSelector = IssuableTemplateSelector;
+})(window);
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
new file mode 100644
index 00000000000..bd8cdde033e
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -0,0 +1,29 @@
+((global) => {
+ class IssuableTemplateSelectors {
+ constructor(opts = {}) {
+ this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
+ this.editor = opts.editor || this.initEditor();
+
+ this.$dropdowns.each((i, dropdown) => {
+ let $dropdown = $(dropdown);
+ new IssuableTemplateSelector({
+ pattern: /(\.md)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor
+ });
+ });
+ }
+
+ initEditor() {
+ let editor = $('.markdown-area');
+ // Proxy ace-editor's .setValue to jQuery's .val
+ editor.setValue = editor.val;
+ editor.getValue = editor.val;
+ return editor;
+ }
+ }
+
+ global.IssuableTemplateSelectors = IssuableTemplateSelectors;
+})(window);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 65d362e072c..bad82868ab0 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -141,7 +141,7 @@
$selectbox.hide();
return $value.css('display', '');
},
- clicked: function(user) {
+ clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -149,7 +149,12 @@
if ($dropdown.hasClass('js-filter-bulk-update')) {
return;
}
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (page === 'projects:boards:show') {
+ selectedId = user.id;
+ gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 542a53f0377..897bc49e7df 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -20,3 +20,8 @@
.turn-off { display: block; }
}
}
+
+// Hide element if Vue is still working on rendering it fully.
+[v-cloak="true"] {
+ display: none !important;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 473530cf094..6c3786b49bb 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -164,6 +164,10 @@
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
}
+ &.btn-spam {
+ @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ }
+
&.btn-danger,
&.btn-remove,
&.btn-red {
@@ -200,6 +204,10 @@
position: relative;
top: 2px;
}
+
+ svg, .fa {
+ margin-right: 3px;
+ }
}
.btn-lg {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1f565d78387..8846e08f390 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -62,9 +62,13 @@
position: absolute;
top: 50%;
right: 6px;
- margin-top: -4px;
+ margin-top: -6px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
+ &.fa-spinner {
+ font-size: 16px;
+ margin-top: -8px;
+ }
}
&:hover, {
@@ -412,6 +416,7 @@
font-size: 14px;
a {
+ cursor: pointer;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 7cf4d4fba42..07c8874bf03 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -6,11 +6,11 @@
table-layout: fixed;
pre {
- padding: 10px;
+ padding: 10px 0;
border: none;
border-radius: 0;
font-family: $monospace_font;
- font-size: $code_font_size !important;
+ font-size: $code_font_size;
line-height: $code_line_height !important;
margin: 0;
overflow: auto;
@@ -20,13 +20,20 @@
border-left: 1px solid;
code {
+ display: inline-block;
+ min-width: 100%;
font-family: $monospace_font;
- white-space: pre;
+ white-space: normal;
word-wrap: normal;
padding: 0;
.line {
- display: inline-block;
+ display: block;
+ width: 100%;
+ min-height: 19px;
+ padding-left: 10px;
+ padding-right: 10px;
+ white-space: pre;
}
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 96565da1bc9..edea4ad00eb 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -147,3 +147,8 @@
color: $gl-link-color;
}
}
+
+.atwho-view small.description {
+ float: right;
+ padding: 3px 5px;
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 5ec5a96a597..d2d60ed7196 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -123,4 +123,9 @@
}
}
}
-} \ No newline at end of file
+}
+
+@mixin dark-diff-match-line {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3fa4a22258d..015fe3debf9 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -222,3 +222,7 @@ header.header-pinned-nav {
padding-right: $sidebar_collapsed_width;
}
}
+
+.right-sidebar {
+ border-left: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ca720022539..5da390118c6 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -276,3 +276,5 @@ $personal-access-tokens-disabled-label-color: #bbb;
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
+
+$issue-boards-font-size: 15px;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 77a73dc379b..16ffbe57a99 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #557;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 80a509a7c1a..7de920e074b 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #49483e;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index c62bd021aef..b11499c71ee 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #174652;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 524cfaf90c3..657bb5e3cd9 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,4 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
+
+@mixin matchLine {
+ color: $black-transparent;
+ background: rgba(255, 255, 255, 0.4);
+}
+
.code.solarized-light {
// Line numbers
.line-numbers, .diff-line-num {
@@ -21,6 +27,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include matchLine;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #ddd8c5;
@@ -36,8 +46,7 @@
}
.line_content.match {
- color: $black-transparent;
- background: rgba(255, 255, 255, 0.4);
+ @include matchLine;
}
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 31a4e3deaac..36a80a916b2 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,4 +1,10 @@
/* https://github.com/aahan/pygments-github-style */
+
+@mixin matchLine {
+ color: $black-transparent;
+ background-color: $match-line;
+}
+
.code.white {
// Line numbers
.line-numbers, .diff-line-num {
@@ -22,6 +28,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include matchLine;
+ }
+
.diff-line-num {
&.old {
background-color: $line-number-old;
@@ -57,8 +67,7 @@
}
&.match {
- color: $black-transparent;
- background-color: $match-line;
+ @include matchLine;
}
&.hll:not(.empty-cell) {
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 33aedf1f7c1..5bfe9bcb443 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -45,7 +45,6 @@
.line_content {
padding-left: 0.5em;
padding-right: 0.5em;
- white-space: pre;
&.old {
background-color: $line-removed;
@@ -71,6 +70,10 @@
}
}
+pre {
+ margin: 0;
+}
+
span.highlight_word {
background-color: #fafe3d !important;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 5607239d92d..c9cdfdcd29c 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -72,7 +72,6 @@
margin-bottom: 20px;
}
-
// Users List
.users-list {
@@ -98,3 +97,44 @@
}
}
}
+
+.abuse-reports {
+ .table {
+ table-layout: fixed;
+ }
+ .subheading {
+ padding-bottom: $gl-padding;
+ }
+ .message {
+ word-wrap: break-word;
+ }
+ .btn {
+ white-space: normal;
+ padding: $gl-btn-padding;
+ }
+ th {
+ width: 15%;
+ &.wide {
+ width: 55%;
+ }
+ }
+ @media (max-width: $screen-sm-max) {
+ th {
+ width: 100%;
+ }
+ td {
+ width: 100%;
+ float: left;
+ }
+ }
+
+ .no-reports {
+ .emoji-icon {
+ margin-left: $btn-side-margin;
+ margin-top: 3px;
+ }
+ span {
+ font-size: 19px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
new file mode 100644
index 00000000000..ad4b2d6496f
--- /dev/null
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -0,0 +1,329 @@
+[v-cloak] {
+ display: none;
+}
+
+.user-can-drag {
+ cursor: -webkit-grab;
+ cursor: grab;
+}
+
+.is-dragging {
+ * {
+ cursor: -webkit-grabbing;
+ cursor: grabbing;
+ }
+}
+
+.dropdown-menu-issues-board-new {
+ width: 320px;
+
+ .dropdown-content {
+ max-height: 150px;
+ }
+}
+
+.issue-board-dropdown-content {
+ margin: 0 8px 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid $dropdown-divider-color;
+
+ > p {
+ margin: 0;
+ font-size: 14px;
+ color: #9c9c9c;
+ }
+}
+
+.issue-boards-page {
+ .content-wrapper {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
+
+ .sub-nav,
+ .issues-filters {
+ -webkit-flex: none;
+ flex: none;
+ }
+
+ .page-with-sidebar {
+ display: -webkit-flex;
+ display: flex;
+ min-height: 100vh;
+ max-height: 100vh;
+ padding-bottom: 0;
+ }
+
+ .issue-boards-content {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ width: 100%;
+
+ .content {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ }
+ }
+}
+
+.boards-app-loading {
+ width: 100%;
+ font-size: 34px;
+}
+
+.boards-list {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ -webkit-flex-basis: 0;
+ flex-basis: 0;
+ min-height: calc(100vh - 152px);
+ max-height: calc(100vh - 152px);
+ padding-top: 25px;
+ padding-right: ($gl-padding / 2);
+ padding-left: ($gl-padding / 2);
+ overflow-x: scroll;
+
+ @media (min-width: $screen-sm-min) {
+ min-height: 475px;
+ max-height: none;
+ }
+}
+
+.board {
+ display: -webkit-flex;
+ display: flex;
+ min-width: calc(100vw - 15px);
+ max-width: calc(100vw - 15px);
+ margin-bottom: 25px;
+ padding-right: ($gl-padding / 2);
+ padding-left: ($gl-padding / 2);
+
+ @media (min-width: $screen-sm-min) {
+ min-width: 400px;
+ max-width: 400px;
+ }
+}
+
+.board-inner {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ font-size: $issue-boards-font-size;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
+.board-header {
+ border-top-left-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
+
+ &.has-border {
+ border-top: 3px solid;
+
+ .board-title {
+ padding-top: ($gl-padding - 3px);
+ }
+ }
+}
+
+.board-header-loading-spinner {
+ margin-right: 10px;
+ color: $gray-darkest;
+}
+
+.board-inner-container {
+ border-bottom: 1px solid $border-color;
+ padding: $gl-padding;
+}
+
+.board-title {
+ position: relative;
+ margin: 0;
+ padding: $gl-padding;
+ font-size: 1em;
+ border-bottom: 1px solid $border-color;
+
+ .board-mobile-handle {
+ position: relative;
+ left: 0;
+ top: 1px;
+ margin-top: 0;
+ margin-right: 5px;
+ }
+}
+
+.board-search-container {
+ position: relative;
+ background-color: #fff;
+
+ .form-control {
+ padding-right: 30px;
+ }
+}
+
+.board-search-icon,
+.board-search-clear-btn {
+ position: absolute;
+ right: $gl-padding + 10px;
+ top: 50%;
+ margin-top: -7px;
+ font-size: 14px;
+}
+
+.board-search-icon {
+ color: $gl-placeholder-color;
+}
+
+.board-search-clear-btn {
+ padding: 0;
+ line-height: 1;
+ background: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+}
+
+.board-delete {
+ margin-right: 10px;
+ padding: 0;
+ color: $gray-darkest;
+ background-color: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+}
+
+.board-blank-state {
+ height: 100%;
+ padding: $gl-padding;
+ background-color: #fff;
+}
+
+.board-blank-state-list {
+ list-style: none;
+
+ > li:not(:last-child) {
+ margin-bottom: 8px;
+ }
+
+ .label-color {
+ position: relative;
+ top: 2px;
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-right: 3px;
+ border-radius: $border-radius-default;
+ }
+}
+
+.board-list {
+ -webkit-flex: 1;
+ flex: 1;
+ height: 400px;
+ margin-bottom: 0;
+ padding: 5px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.board-list-loading {
+ margin-top: 10px;
+ font-size: 26px;
+}
+
+.is-ghost {
+ opacity: 0.3;
+}
+
+.is-dragging {
+ // Important because plugin sets inline CSS
+ opacity: 1!important;
+}
+
+.card {
+ position: relative;
+ width: 100%;
+ padding: 10px $gl-padding;
+ background: #fff;
+ border-radius: $border-radius-default;
+ box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
+ list-style: none;
+
+ &.user-can-drag {
+ padding-left: ($gl-padding * 2);
+
+ @media (min-width: $screen-sm-min) {
+ padding-left: $gl-padding;
+ }
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+
+ a {
+ cursor: pointer;
+ }
+
+ .label {
+ border: 0;
+ outline: 0;
+ }
+
+ .confidential-icon {
+ margin-right: 5px;
+ }
+}
+
+.board-mobile-handle {
+ position: absolute;
+ left: 10px;
+ top: 50%;
+ margin-top: (-15px / 2);
+
+ @media (min-width: $screen-sm-min) {
+ display: none;
+ }
+}
+
+.card-title {
+ margin: 0;
+ font-size: 1em;
+
+ a {
+ color: inherit;
+ }
+}
+
+.card-footer {
+ margin-top: 5px;
+
+ .label {
+ margin-right: 4px;
+ font-size: (14px / $issue-boards-font-size) * 1em;
+ }
+}
+
+.card-number {
+ margin-right: 8px;
+ font-weight: 500;
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e26f8f7080d..81fce55853c 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -53,14 +53,6 @@
left: 70px;
}
}
-
- .nav-links {
- svg {
- position: relative;
- top: 2px;
- margin-right: 3px;
- }
- }
}
.build-header {
@@ -108,24 +100,98 @@
}
.right-sidebar.build-sidebar {
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
+ padding: $gl-padding 0;
&.right-sidebar-collapsed {
display: none;
}
+ .blocks-container {
+ padding: $gl-padding;
+ }
+
.block {
width: 100%;
}
.build-sidebar-header {
- padding-top: 0;
+ padding: 0 $gl-padding $gl-padding;
.gutter-toggle {
margin-top: 0;
}
}
+
+ .stage-item {
+ cursor: pointer;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ .build-dropdown {
+ padding: 0 $gl-padding;
+
+ .dropdown-menu-toggle {
+ margin-top: 8px;
+ }
+
+ .dropdown-menu {
+ right: $gl-padding;
+ left: $gl-padding;
+ width: auto;
+ }
+ }
+
+ .builds-container {
+ margin-top: $gl-padding;
+ background-color: $white-light;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ max-height: 300px;
+ overflow: scroll;
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 3px;
+ height: 13px;
+ }
+
+ a {
+ display: block;
+ padding: $gl-padding 10px $gl-padding 40px;
+ width: 270px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover {
+ background-color: $row-hover;
+ color: $gl-text-color;
+ }
+ }
+
+ .build-job {
+ position: relative;
+
+ .fa {
+ position: absolute;
+ left: 15px;
+ top: 20px;
+ display: none;
+ }
+
+ &.active {
+ font-weight: bold;
+
+ .fa {
+ display: block;
+ }
+ }
+ }
+ }
}
.build-detail-row {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 7a50bc9c832..46c4a11aa2e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -395,3 +395,12 @@
display: inline-block;
line-height: 18px;
}
+
+.js-issuable-selector-wrap {
+ .js-issuable-selector {
+ width: 100%;
+ }
+ @media (max-width: $screen-sm-max) {
+ margin-bottom: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
new file mode 100644
index 00000000000..1f499897c16
--- /dev/null
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -0,0 +1,238 @@
+$colors: (
+ white_header_head_neutral : #e1fad7,
+ white_line_head_neutral : #effdec,
+ white_button_head_neutral : #9adb84,
+
+ white_header_head_chosen : #baf0a8,
+ white_line_head_chosen : #e1fad7,
+ white_button_head_chosen : #52c22d,
+
+ white_header_origin_neutral : #e0f0ff,
+ white_line_origin_neutral : #f2f9ff,
+ white_button_origin_neutral : #87c2fa,
+
+ white_header_origin_chosen : #add8ff,
+ white_line_origin_chosen : #e0f0ff,
+ white_button_origin_chosen : #268ced,
+
+ white_header_not_chosen : #f0f0f0,
+ white_line_not_chosen : #f9f9f9,
+
+
+ dark_header_head_neutral : rgba(#3f3, .2),
+ dark_line_head_neutral : rgba(#3f3, .1),
+ dark_button_head_neutral : #40874f,
+
+ dark_header_head_chosen : rgba(#3f3, .33),
+ dark_line_head_chosen : rgba(#3f3, .2),
+ dark_button_head_chosen : #258537,
+
+ dark_header_origin_neutral : rgba(#2878c9, .4),
+ dark_line_origin_neutral : rgba(#2878c9, .3),
+ dark_button_origin_neutral : #2a5c8c,
+
+ dark_header_origin_chosen : rgba(#2878c9, .6),
+ dark_line_origin_chosen : rgba(#2878c9, .4),
+ dark_button_origin_chosen : #1d6cbf,
+
+ dark_header_not_chosen : rgba(#fff, .25),
+ dark_line_not_chosen : rgba(#fff, .1),
+
+
+ monokai_header_head_neutral : rgba(#a6e22e, .25),
+ monokai_line_head_neutral : rgba(#a6e22e, .1),
+ monokai_button_head_neutral : #376b20,
+
+ monokai_header_head_chosen : rgba(#a6e22e, .4),
+ monokai_line_head_chosen : rgba(#a6e22e, .25),
+ monokai_button_head_chosen : #39800d,
+
+ monokai_header_origin_neutral : rgba(#60d9f1, .35),
+ monokai_line_origin_neutral : rgba(#60d9f1, .15),
+ monokai_button_origin_neutral : #38848c,
+
+ monokai_header_origin_chosen : rgba(#60d9f1, .5),
+ monokai_line_origin_chosen : rgba(#60d9f1, .35),
+ monokai_button_origin_chosen : #3ea4b2,
+
+ monokai_header_not_chosen : rgba(#76715d, .24),
+ monokai_line_not_chosen : rgba(#76715d, .1),
+
+
+ solarized_light_header_head_neutral : rgba(#859900, .37),
+ solarized_light_line_head_neutral : rgba(#859900, .2),
+ solarized_light_button_head_neutral : #afb262,
+
+ solarized_light_header_head_chosen : rgba(#859900, .5),
+ solarized_light_line_head_chosen : rgba(#859900, .37),
+ solarized_light_button_head_chosen : #94993d,
+
+ solarized_light_header_origin_neutral : rgba(#2878c9, .37),
+ solarized_light_line_origin_neutral : rgba(#2878c9, .15),
+ solarized_light_button_origin_neutral : #60a1bf,
+
+ solarized_light_header_origin_chosen : rgba(#2878c9, .6),
+ solarized_light_line_origin_chosen : rgba(#2878c9, .37),
+ solarized_light_button_origin_chosen : #2482b2,
+
+ solarized_light_header_not_chosen : rgba(#839496, .37),
+ solarized_light_line_not_chosen : rgba(#839496, .2),
+
+
+ solarized_dark_header_head_neutral : rgba(#859900, .35),
+ solarized_dark_line_head_neutral : rgba(#859900, .15),
+ solarized_dark_button_head_neutral : #376b20,
+
+ solarized_dark_header_head_chosen : rgba(#859900, .5),
+ solarized_dark_line_head_chosen : rgba(#859900, .35),
+ solarized_dark_button_head_chosen : #39800d,
+
+ solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
+ solarized_dark_line_origin_neutral : rgba(#2878c9, .15),
+ solarized_dark_button_origin_neutral : #086799,
+
+ solarized_dark_header_origin_chosen : rgba(#2878c9, .6),
+ solarized_dark_line_origin_chosen : rgba(#2878c9, .35),
+ solarized_dark_button_origin_chosen : #0082cc,
+
+ solarized_dark_header_not_chosen : rgba(#839496, .25),
+ solarized_dark_line_not_chosen : rgba(#839496, .15)
+);
+
+
+@mixin color-scheme($color) {
+ .header.line_content, .diff-line-num {
+ &.origin {
+ background-color: map-get($colors, #{$color}_header_origin_neutral);
+ border-color: map-get($colors, #{$color}_header_origin_neutral);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_origin_neutral);
+ border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
+ }
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_header_origin_chosen);
+ border-color: map-get($colors, #{$color}_header_origin_chosen);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_origin_chosen);
+ border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
+ }
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_header_not_chosen);
+ border-color: map-get($colors, #{$color}_header_not_chosen);
+
+ button {
+ background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
+ border-color: map-get($colors, #{$color}_button_origin_neutral);
+ }
+ }
+ }
+ &.head {
+ background-color: map-get($colors, #{$color}_header_head_neutral);
+ border-color: map-get($colors, #{$color}_header_head_neutral);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_head_neutral);
+ border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
+ }
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_header_head_chosen);
+ border-color: map-get($colors, #{$color}_header_head_chosen);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_head_chosen);
+ border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
+ }
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_header_not_chosen);
+ border-color: map-get($colors, #{$color}_header_not_chosen);
+
+ button {
+ background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
+ border-color: map-get($colors, #{$color}_button_head_neutral);
+ }
+ }
+ }
+ }
+
+ .line_content {
+ &.origin {
+ background-color: map-get($colors, #{$color}_line_origin_neutral);
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_line_origin_chosen);
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_line_not_chosen);
+ }
+ }
+ &.head {
+ background-color: map-get($colors, #{$color}_line_head_neutral);
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_line_head_chosen);
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_line_not_chosen);
+ }
+ }
+ }
+}
+
+
+#conflicts {
+
+ .white {
+ @include color-scheme('white')
+ }
+
+ .dark {
+ @include color-scheme('dark')
+ }
+
+ .monokai {
+ @include color-scheme('monokai')
+ }
+
+ .solarized-light {
+ @include color-scheme('solarized_light')
+ }
+
+ .solarized-dark {
+ @include color-scheme('solarized_dark')
+ }
+
+ .diff-wrap-lines .line_content {
+ white-space: normal;
+ min-height: 19px;
+ }
+
+ .line_content.header {
+ position: relative;
+
+ button {
+ border-radius: 2px;
+ font-size: 10px;
+ position: absolute;
+ right: 10px;
+ padding: 0;
+ outline: none;
+ color: #fff;
+ width: 75px; // static width to make 2 buttons have same width
+ height: 19px;
+ }
+ }
+
+ .btn-success .fa-spinner {
+ color: #fff;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7d8f58ca5af..abe8414e5e0 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -69,6 +69,10 @@
&.ci-success {
color: $gl-success;
+
+ a.environment {
+ color: inherit;
+ }
}
&.ci-success_with_warnings {
@@ -126,7 +130,6 @@
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
}
-
}
p:last-child {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3784010348a..bd875b9823f 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -159,6 +159,32 @@
}
}
+.discussion-with-resolve-btn {
+ display: table;
+ width: 100%;
+ border-collapse: separate;
+ table-layout: auto;
+
+ .btn-group {
+ display: table-cell;
+ float: none;
+ width: 1%;
+
+ &:first-child {
+ width: 100%;
+ padding-right: 5px;
+ }
+
+ &:last-child {
+ padding-left: 5px;
+ }
+ }
+
+ .btn {
+ width: 100%;
+ }
+}
+
.discussion-notes-count {
font-size: 16px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a2b5437e503..08d1692c888 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -383,3 +383,80 @@ ul.notes {
color: $gl-link-color;
}
}
+
+.line-resolve-all-container {
+ .btn-group {
+ margin-top: -1px;
+ margin-left: -4px;
+ }
+
+ .discussion-next-btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.line-resolve-all {
+ display: inline-block;
+ padding: 5px 10px;
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ &.has-next-btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .line-resolve-btn {
+ vertical-align: middle;
+ margin-right: 5px;
+ }
+}
+
+.line-resolve-text {
+ vertical-align: middle;
+}
+
+.line-resolve-btn {
+ display: inline-block;
+ position: relative;
+ top: 2px;
+ padding: 0;
+ background-color: transparent;
+ border: none;
+ outline: 0;
+
+ &.is-disabled {
+ cursor: default;
+ }
+
+ &:not(.is-disabled):hover,
+ &:not(.is-disabled):focus,
+ &.is-active {
+ color: $gl-text-green;
+
+ svg path {
+ fill: $gl-text-green;
+ }
+ }
+
+ svg {
+ position: relative;
+ color: $notes-action-color;
+
+ path {
+ fill: $notes-action-color;
+ }
+ }
+}
+
+.discussion-next-btn {
+ svg {
+ margin: 0;
+
+ path {
+ fill: $gray-darkest;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 21919fe4d73..ce1c424624f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -229,3 +229,196 @@
box-shadow: none;
}
}
+
+// Pipeline visualization
+
+.toggle-pipeline-btn {
+ background-color: $gray-dark;
+
+ .caret {
+ border-top: none;
+ border-bottom: 4px solid;
+ }
+
+ &.graph-collapsed {
+ background-color: $white-light;
+
+ .caret {
+ border-bottom: none;
+ border-top: 4px solid;
+ }
+ }
+}
+
+.pipeline-graph {
+ width: 100%;
+ overflow: auto;
+ white-space: nowrap;
+ max-height: 500px;
+ transition: max-height 0.3s, padding 0.3s;
+
+ &.graph-collapsed {
+ max-height: 0;
+ padding: 0 16px;
+ }
+}
+
+.pipeline-visualization {
+ position: relative;
+ min-width: 1220px;
+
+ ul {
+ padding: 0;
+ }
+}
+
+.stage-column {
+ display: inline-block;
+ vertical-align: top;
+ margin-right: 50px;
+
+ li {
+ list-style: none;
+ }
+
+ .stage-name {
+ margin-bottom: 15px;
+ font-weight: bold;
+ width: 150px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .build {
+ border: 1px solid $border-color;
+ position: relative;
+ padding: 6px 10px;
+ border-radius: 30px;
+ width: 150px;
+ margin-bottom: 10px;
+
+ &.playable {
+ background-color: $gray-light;
+ }
+
+ .build-content {
+ width: 130px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ a {
+ color: $layout-link-gray;
+ }
+ }
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ }
+
+ .fa {
+ font-size: 13px;
+ }
+
+ // Connect first build in each stage with right horizontal line
+ &:first-child {
+ &::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: -54px;
+ border-top: 2px solid $border-color;
+ width: 54px;
+ height: 1px;
+ }
+ }
+
+ // Connect each build (except for first) with curved lines
+ &:not(:first-child) {
+ &::after, &::before {
+ content: '';
+ top: -47px;
+ position: absolute;
+ border-bottom: 2px solid $border-color;
+ width: 20px;
+ height: 65px;
+ }
+
+ // Right connecting curves
+ &::after {
+ right: -20px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 0 50px;
+ }
+
+ // Left connecting curves
+ &::before {
+ left: -20px;
+ border-left: 2px solid $border-color;
+ border-radius: 0 0 0 50px;
+ }
+ }
+
+ // Connect second build to first build with smaller curved line
+ &:nth-child(2) {
+ &::after, &::before {
+ height: 45px;
+ top: -26px;
+ }
+ }
+ }
+
+ &:last-child {
+ .build {
+ // Remove right connecting horizontal line from first build in last stage
+ &:first-child {
+ &::after, &::before {
+ border: none;
+ }
+ }
+ // Remove right curved connectors from all builds in last stage
+ &:not(:first-child) {
+ &::after {
+ border: none;
+ }
+ }
+ }
+ }
+
+ &:first-child {
+ .build {
+ // Remove left curved connectors from all builds in first stage
+ &:not(:first-child) {
+ &::before {
+ border: none;
+ }
+ }
+ }
+ }
+}
+
+.pipeline-actions {
+ border-bottom: none;
+}
+
+.toggle-pipeline-btn {
+
+ .fa {
+ color: $dropdown-header-color;
+ }
+}
+
+.pipelines.tab-pane {
+
+ .content-list.pipelines {
+ overflow: scroll;
+ }
+
+ .stage {
+ max-width: 60px;
+ width: 60px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 46371ec6871..6f58203f49c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -228,3 +228,9 @@
}
}
}
+
+table.u2f-registrations {
+ th:not(:last-child), td:not(:last-child) {
+ border-right: solid 1px transparent;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cf9aa02600d..27dc2b2a1fa 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -99,7 +99,7 @@
margin-left: auto;
margin-right: auto;
margin-bottom: 15px;
- max-width: 480px;
+ max-width: 700px;
> p {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index cf16d070cfe..0340526a53a 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -20,10 +20,43 @@
}
}
-.todo {
+.todos-list > .todo {
+ // workaround because we cannot use border-colapse
+ border-top: 1px solid transparent;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+
&:hover {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
cursor: pointer;
}
+
+ // overwrite border style of .content-list
+ &:last-child {
+ border-bottom: 1px solid transparent;
+
+ &:hover {
+ border-color: $row-hover-border;
+ }
+ }
+
+ .todo-actions {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ margin-left: 10px;
+ }
+
+ .todo-item {
+ -webkit-flex: auto;
+ flex: auto;
+ }
}
.todo-item {
@@ -43,8 +76,6 @@
}
.todo-body {
- margin-right: 174px;
-
.todo-note {
word-wrap: break-word;
@@ -90,6 +121,12 @@
}
@media (max-width: $screen-xs-max) {
+ .todo {
+ .avatar {
+ display: none;
+ }
+ }
+
.todo-item {
.todo-title {
white-space: normal;
@@ -98,10 +135,6 @@
margin-bottom: 10px;
}
- .avatar {
- display: none;
- }
-
.todo-body {
margin: 0;
border-left: 2px solid #ddd;