summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/new_repo.pngbin0 -> 19292 bytes
-rw-r--r--app/assets/images/old_repo.pngbin0 -> 20668 bytes
-rw-r--r--app/assets/javascripts/api.js16
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js5
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js4
-rw-r--r--app/assets/javascripts/build.js1
-rw-r--r--app/assets/javascripts/build_variables.js2
-rw-r--r--app/assets/javascripts/commons/bootstrap.js2
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/javascripts/dispatcher.js19
-rw-r--r--app/assets/javascripts/fly_out_nav.js1
-rw-r--r--app/assets/javascripts/gl_dropdown.js67
-rw-r--r--app/assets/javascripts/helpers/user_feature_helper.js11
-rw-r--r--app/assets/javascripts/new_sidebar.js42
-rw-r--r--app/assets/javascripts/pdf/index.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue4
-rw-r--r--app/assets/javascripts/project.js8
-rw-r--r--app/assets/javascripts/project_select.js17
-rw-r--r--app/assets/javascripts/project_select_combo_button.js85
-rw-r--r--app/assets/javascripts/projects/project_import_gitlab_project.js14
-rw-r--r--app/assets/javascripts/projects/project_new.js20
-rw-r--r--app/assets/javascripts/repo/components/repo.vue63
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue100
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue135
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue66
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue42
-rw-r--r--app/assets/javascripts/repo/components/repo_file_options.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue51
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue26
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue32
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue104
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue45
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue43
-rw-r--r--app/assets/javascripts/repo/helpers/monaco_loader_helper.js21
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js303
-rw-r--r--app/assets/javascripts/repo/index.js74
-rw-r--r--app/assets/javascripts/repo/mixins/repo_mixin.js17
-rw-r--r--app/assets/javascripts/repo/monaco_loader.js13
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js82
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js241
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue82
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue47
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue45
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js18
-rw-r--r--app/assets/javascripts/test_utils/index.js2
-rw-r--r--app/assets/javascripts/users/activity_calendar.js12
-rw-r--r--app/assets/javascripts/users/user_tabs.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js100
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js81
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js79
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js95
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js154
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js178
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue67
-rw-r--r--app/assets/javascripts/wikis.js2
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss6
-rw-r--r--app/assets/stylesheets/framework/layout.scss22
-rw-r--r--app/assets/stylesheets/framework/media_object.scss8
-rw-r--r--app/assets/stylesheets/framework/nav.scss26
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss5
-rw-r--r--app/assets/stylesheets/framework/variables.scss23
-rw-r--r--app/assets/stylesheets/new_nav.scss4
-rw-r--r--app/assets/stylesheets/new_sidebar.scss183
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/builds.scss19
-rw-r--r--app/assets/stylesheets/pages/issuable.scss32
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss317
-rw-r--r--app/assets/stylesheets/pages/note_form.scss53
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss135
-rw-r--r--app/assets/stylesheets/pages/repo.scss413
-rw-r--r--app/assets/stylesheets/pages/settings.scss20
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss12
104 files changed, 3778 insertions, 957 deletions
diff --git a/app/assets/images/new_repo.png b/app/assets/images/new_repo.png
new file mode 100644
index 00000000000..ed3af06ab1d
--- /dev/null
+++ b/app/assets/images/new_repo.png
Binary files differ
diff --git a/app/assets/images/old_repo.png b/app/assets/images/old_repo.png
new file mode 100644
index 00000000000..c3c3b791ad9
--- /dev/null
+++ b/app/assets/images/old_repo.png
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 56fa0d71a9a..76b724e1bcb 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -13,6 +13,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
+ commitPath: '/api/:version/projects/:id/repository/commits',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
@@ -95,6 +96,21 @@ const Api = {
.done(projects => callback(projects));
},
+ commitMultiple(id, data, callback) {
+ // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
+ const url = Api.buildUrl(Api.commitPath)
+ .replace(':id', id);
+ return $.ajax({
+ url,
+ type: 'POST',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data),
+ dataType: 'json',
+ })
+ .done(commitData => callback(commitData))
+ .fail(message => callback(message.responseJSON));
+ },
+
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index daef01bc93d..d3de1830895 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -97,9 +97,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
- if (!this.list) return true;
-
- return !this.list.label || label.id !== this.list.label.id;
+ if (!this.list || !label) return true;
+ return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 1d36519c75c..96af69e7a36 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -1,8 +1,8 @@
/* global ListIssue */
import Vue from 'vue';
-import queryData from '../../utils/query_data';
-import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import queryData from '~/boards/utils/query_data';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index b3d3bbcf84f..940326dcd33 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -164,7 +164,6 @@ window.Build = (function () {
Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar');
- this.$sidebar.niceScroll();
};
Build.prototype.getBuildTrace = function () {
diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js
index 99082b412e2..c955a9ac2ea 100644
--- a/app/assets/javascripts/build_variables.js
+++ b/app/assets/javascripts/build_variables.js
@@ -2,7 +2,7 @@
$(function() {
$('.reveal-variables').off('click').on('click', function() {
- $('.js-build').toggle().niceScroll();
+ $('.js-build-variables').toggle();
$(this).hide();
});
});
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index 389587a2596..c11b7d5f340 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -3,13 +3,13 @@ import $ from 'jquery';
// bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
+import 'bootstrap-sass/assets/javascripts/bootstrap/button';
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
-import 'bootstrap-sass/assets/javascripts/bootstrap/button';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index b53f6284afc..b93e94a3c97 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -6,6 +6,5 @@ import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
-import 'vendor/jquery.nicescroll';
import 'vendor/jquery.waitforimages';
import 'select2/select2';
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index ad5ff19ec58..265e304b957 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -75,6 +75,7 @@ import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import GpgBadges from './gpg_badges';
+import UserFeatureHelper from './helpers/user_feature_helper';
(function() {
var Dispatcher;
@@ -92,6 +93,7 @@ import GpgBadges from './gpg_badges';
if (!page) {
return false;
}
+
path = page.split(':');
shortcut_handler = null;
@@ -332,22 +334,20 @@ import GpgBadges from './gpg_badges';
break;
case 'projects:commits:show':
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit);
- new gl.Activities();
shortcut_handler = new ShortcutsNavigation();
GpgBadges.fetch();
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
- if ($('#tree-slider').length) {
- new TreeView();
- }
- if ($('.blob-viewer').length) {
- new BlobViewer();
- }
+
+ if ($('#tree-slider').length) new TreeView();
+ if ($('.blob-viewer').length) new BlobViewer();
break;
case 'projects:edit':
setupProjectEdit();
+ // Initialize expandable settings panels
+ initSettingsPanels();
break;
case 'projects:imports:show':
new ProjectImport();
@@ -406,6 +406,9 @@ import GpgBadges from './gpg_badges';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
+
+ if (UserFeatureHelper.isNewRepo()) break;
+
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
@@ -424,6 +427,7 @@ import GpgBadges from './gpg_badges';
shortcut_handler = true;
break;
case 'projects:blob:show':
+ if (UserFeatureHelper.isNewRepo()) break;
new BlobViewer();
initBlob();
break;
@@ -576,7 +580,6 @@ import GpgBadges from './gpg_badges';
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'), true);
- new Sidebar();
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 8e9a97fe207..301e82f4610 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -23,6 +23,7 @@ export const showSubLevelItems = (el) => {
const top = calculateTop(boundingRect, subItems.offsetHeight);
const isAbove = top < boundingRect.top;
+ subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`;
if (isAbove) {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 7d11cd0b6b2..b62acfcd445 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,9 +1,53 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
+/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
import { isObject } from './lib/utils/type_utility';
-var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote;
+var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
+
+GitLabDropdownInput = (function() {
+ function GitLabDropdownInput(input, options) {
+ var $inputContainer, $clearButton;
+ var _this = this;
+ this.input = input;
+ this.options = options;
+ this.fieldName = this.options.fieldName || 'field-name';
+ $inputContainer = this.input.parent();
+ $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', (function(_this) {
+ // Clear click
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input.val('').trigger('input').focus();
+ };
+ })(this));
+
+ this.input
+ .on('keydown', function (e) {
+ var keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', function(e) {
+ var val = e.currentTarget.value || _this.options.inputFieldName;
+ val = val.split(' ').join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ _this.cb(_this.options.fieldName, val, {}, true);
+ _this.input.closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
+ }
+
+ GitLabDropdownInput.prototype.onInput = function(cb) {
+ this.cb = cb;
+ };
+
+ return GitLabDropdownInput;
+})();
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
@@ -191,7 +235,7 @@ GitLabDropdownRemote = (function() {
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading";
@@ -209,7 +253,9 @@ GitLabDropdown = (function() {
CURSOR_SELECT_SCROLL_PADDING = 5;
- FILTER_INPUT = '.dropdown-input .dropdown-input-field';
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
+
+ NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
function GitLabDropdown(el1, options) {
var searchFields, selector, self;
@@ -224,6 +270,7 @@ GitLabDropdown = (function() {
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
@@ -262,6 +309,10 @@ GitLabDropdown = (function() {
});
}
}
+ if (this.noFilterInput.length) {
+ this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
+ this.plainInput.onInput(this.addInput.bind(this));
+ }
// Init filterable
if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, {
@@ -753,9 +804,13 @@ GitLabDropdown = (function() {
}
};
- GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) {
var $input;
// Create hidden input for form
+ if (single) {
+ $('input[name="' + fieldName + '"]').remove();
+ }
+
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
@@ -771,7 +826,7 @@ GitLabDropdown = (function() {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
- return this.dropdown.before($input);
+ this.dropdown.before($input).trigger('change');
};
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
diff --git a/app/assets/javascripts/helpers/user_feature_helper.js b/app/assets/javascripts/helpers/user_feature_helper.js
new file mode 100644
index 00000000000..fcd8569819c
--- /dev/null
+++ b/app/assets/javascripts/helpers/user_feature_helper.js
@@ -0,0 +1,11 @@
+import Cookies from 'js-cookie';
+
+function isNewRepo() {
+ return Cookies.get('new_repo') === 'true';
+}
+
+const UserFeatureHelper = {
+ isNewRepo,
+};
+
+export default UserFeatureHelper;
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index 5f98aff8ced..930218dd1f5 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -1,23 +1,65 @@
+import Cookies from 'js-cookie';
+import _ from 'underscore';
+/* global bp */
+import './breakpoints';
+
export default class NewNavSidebar {
constructor() {
this.initDomElements();
+ this.render();
}
initDomElements() {
+ this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar');
this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
+ this.$sidebarToggle = $('.js-toggle-sidebar');
}
bindEvents() {
this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
+ this.$sidebarToggle.on('click', () => {
+ const value = !this.$sidebar.hasClass('sidebar-icons-only');
+ this.toggleCollapsedSidebar(value);
+ });
+
+ $(window).on('resize', () => _.debounce(this.render(), 100));
+ }
+
+ static setCollapsedCookie(value) {
+ if (bp.getBreakpointSize() !== 'lg') {
+ return;
+ }
+ Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
}
toggleSidebarNav(show) {
this.$sidebar.toggleClass('nav-sidebar-expanded', show);
this.$overlay.toggleClass('mobile-nav-open', show);
+ this.$sidebar.removeClass('sidebar-icons-only');
+ }
+
+ toggleCollapsedSidebar(collapsed) {
+ this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ if (this.$sidebar.length) {
+ this.$page.toggleClass('page-with-new-sidebar', !collapsed);
+ this.$page.toggleClass('page-with-icon-sidebar', collapsed);
+ }
+ NewNavSidebar.setCollapsedCookie(collapsed);
+ }
+
+ render() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'sm' || breakpoint === 'md') {
+ this.toggleCollapsedSidebar(true);
+ } else if (breakpoint === 'lg') {
+ const collapse = Cookies.get('sidebar_collapsed') === 'true';
+ this.toggleCollapsedSidebar(collapse);
+ }
}
}
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index 4603859d7b0..b874e484d45 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -9,8 +9,8 @@
</template>
<script>
- import pdfjsLib from 'pdfjs-dist';
- import workerSrc from 'vendor/pdf.worker';
+ import pdfjsLib from 'vendor/pdf';
+ import workerSrc from 'vendor/pdf.worker.min';
import page from './page/index.vue';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 77cbaeb43ef..66bc1d1979c 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,7 +1,7 @@
<script>
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import '~/flash';
import stageColumnComponent from './stage_column_component.vue';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
- import '../../../flash';
export default {
props: {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 6e1744e8e72..1c2100a1c25 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -90,6 +90,7 @@ import Cookies from 'js-cookie';
filterable: true,
filterRemote: true,
filterByText: true,
+ inputFieldName: $dropdown.data('input-field-name'),
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var li = refListItem.cloneNode(false);
@@ -123,9 +124,14 @@ import Cookies from 'js-cookie';
e.preventDefault();
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
+
+ var $visit = $dropdown.data('visit');
+ var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
- gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
+ if (shouldVisit) {
+ gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
+ }
}
}
});
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index ebcefc819f5..1b4ed6be90a 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import Api from './api';
+import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
@@ -58,7 +59,8 @@ import Api from './api';
if (this.includeGroups) {
placeholder += " or group";
}
- return $(select).select2({
+
+ $(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function(_this) {
@@ -96,21 +98,18 @@ import Api from './api';
};
})(this),
id: function(project) {
- return project.web_url;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
},
text: function(project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
- });
-
- $('.new-project-item-select-button').on('click', function() {
- $('.project-item-select', this.parentNode).select2('open');
- });
- $('.project-item-select').on('click', function() {
- window.location = `${$(this).val()}/${this.dataset.relativePath}`;
+ return new ProjectSelectComboButton(select);
});
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
new file mode 100644
index 00000000000..f799d9d619a
--- /dev/null
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -0,0 +1,85 @@
+import AccessorUtilities from './lib/utils/accessor';
+
+export default class ProjectSelectComboButton {
+ constructor(select) {
+ this.projectSelectInput = $(select);
+ this.newItemBtn = $('.new-project-item-link');
+ this.newItemBtnBaseText = this.newItemBtn.data('label');
+ this.itemType = this.deriveItemTypeFromLabel();
+ this.groupId = this.projectSelectInput.data('groupId');
+
+ this.bindEvents();
+ this.initLocalStorage();
+ }
+
+ bindEvents() {
+ this.projectSelectInput.siblings('.new-project-item-select-button')
+ .on('click', this.openDropdown);
+
+ this.projectSelectInput.on('change', () => this.selectProject());
+ }
+
+ initLocalStorage() {
+ const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
+
+ if (localStorageIsSafe) {
+ const itemTypeKebabed = this.newItemBtnBaseText.toLowerCase().split(' ').join('-');
+
+ this.localStorageKey = ['group', this.groupId, itemTypeKebabed, 'recent-project'].join('-');
+ this.setBtnTextFromLocalStorage();
+ }
+ }
+
+ openDropdown() {
+ $(this).siblings('.project-item-select').select2('open');
+ }
+
+ selectProject() {
+ const selectedProjectData = JSON.parse(this.projectSelectInput.val());
+ const projectUrl = `${selectedProjectData.url}/${this.projectSelectInput.data('relativePath')}`;
+ const projectName = selectedProjectData.name;
+
+ const projectMeta = {
+ url: projectUrl,
+ name: projectName,
+ };
+
+ this.setNewItemBtnAttributes(projectMeta);
+ this.setProjectInLocalStorage(projectMeta);
+ }
+
+ setBtnTextFromLocalStorage() {
+ const cachedProjectData = this.getProjectFromLocalStorage();
+
+ this.setNewItemBtnAttributes(cachedProjectData);
+ }
+
+ setNewItemBtnAttributes(project) {
+ if (project) {
+ this.newItemBtn.attr('href', project.url);
+ this.newItemBtn.text(`${this.newItemBtnBaseText} in ${project.name}`);
+ this.newItemBtn.enable();
+ } else {
+ this.newItemBtn.text(`Select project to create ${this.itemType}`);
+ this.newItemBtn.disable();
+ }
+ }
+
+ deriveItemTypeFromLabel() {
+ // label is either 'New issue' or 'New merge request'
+ return this.newItemBtnBaseText.split(' ').slice(1).join(' ');
+ }
+
+ getProjectFromLocalStorage() {
+ const projectString = localStorage.getItem(this.localStorageKey);
+
+ return JSON.parse(projectString);
+ }
+
+ setProjectInLocalStorage(projectMeta) {
+ const projectString = JSON.stringify(projectMeta);
+
+ localStorage.setItem(this.localStorageKey, projectString);
+ }
+}
+
diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js
new file mode 100644
index 00000000000..c34927499fc
--- /dev/null
+++ b/app/assets/javascripts/projects/project_import_gitlab_project.js
@@ -0,0 +1,14 @@
+import '../lib/utils/url_utility';
+
+const bindEvents = () => {
+ const path = gl.utils.getParameterValues('path')[0];
+
+ // get the path url and append it in the inputS
+ $('.js-path-name').val(path);
+};
+
+document.addEventListener('DOMContentLoaded', bindEvents);
+
+export default {
+ bindEvents,
+};
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 1dc1dbf356d..985521aef34 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,7 +1,7 @@
let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
- if ($projectImportUrl.attr('disabled') || hasUserDefinedProjectPath) {
+ if (hasUserDefinedProjectPath) {
return;
}
@@ -27,8 +27,6 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
- const importBtnTooltip = 'Please enter a valid project name.';
- const $importBtnWrapper = $('.import_gitlab_project');
const $projectImportUrl = $('#project_import_url');
const $projectPath = $('#project_path');
@@ -50,31 +48,15 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
});
- $('.btn_import_gitlab_project').attr('disabled', !$projectPath.val().trim().length);
- $importBtnWrapper.attr('title', importBtnTooltip);
-
$newProjectForm.on('submit', () => {
$projectPath.val($projectPath.val().trim());
});
$projectPath.on('keyup', () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
- if (hasUserDefinedProjectPath) {
- $('.btn_import_gitlab_project').attr('disabled', false);
- $importBtnWrapper.attr('title', '');
- $importBtnWrapper.removeClass('has-tooltip');
- } else {
- $('.btn_import_gitlab_project').attr('disabled', true);
- $importBtnWrapper.addClass('has-tooltip');
- }
});
- $projectImportUrl.disable();
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
-
- $('.import_git').on('click', () => {
- $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
- });
};
document.addEventListener('DOMContentLoaded', bindEvents);
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
new file mode 100644
index 00000000000..703da749ad3
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -0,0 +1,63 @@
+<script>
+import RepoSidebar from './repo_sidebar.vue';
+import RepoCommitSection from './repo_commit_section.vue';
+import RepoTabs from './repo_tabs.vue';
+import RepoFileButtons from './repo_file_buttons.vue';
+import RepoPreview from './repo_preview.vue';
+import RepoMixin from '../mixins/repo_mixin';
+import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import Store from '../stores/repo_store';
+import Helper from '../helpers/repo_helper';
+import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
+
+export default {
+ data: () => Store,
+ mixins: [RepoMixin],
+ components: {
+ 'repo-sidebar': RepoSidebar,
+ 'repo-tabs': RepoTabs,
+ 'repo-file-buttons': RepoFileButtons,
+ 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
+ 'repo-commit-section': RepoCommitSection,
+ 'popup-dialog': PopupDialog,
+ 'repo-preview': RepoPreview,
+ },
+
+ mounted() {
+ Helper.getContent().catch(Helper.loadingError);
+ },
+
+ methods: {
+ dialogToggled(toggle) {
+ this.dialog.open = toggle;
+ },
+
+ dialogSubmitted(status) {
+ this.dialog.open = false;
+ this.dialog.status = status;
+ },
+
+ toggleBlobView: Store.toggleBlobView,
+ },
+};
+</script>
+
+<template>
+<div class="repository-view tree-content-holder">
+ <repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
+ <repo-tabs/>
+ <component :is="currentBlobView" class="blob-viewer-container"></component>
+ <repo-file-buttons/>
+ </div>
+ <repo-commit-section/>
+ <popup-dialog
+ :primary-button-label="__('Discard changes')"
+ :open="dialog.open"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :body="__('Are you sure you want to discard your changes?')"
+ @toggle="dialogToggled"
+ @submit="dialogSubmitted"
+ />
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
new file mode 100644
index 00000000000..bd83f80c928
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -0,0 +1,100 @@
+<script>
+/* global Flash */
+import Store from '../stores/repo_store';
+import RepoMixin from '../mixins/repo_mixin';
+import Helper from '../helpers/repo_helper';
+import Service from '../services/repo_service';
+
+const RepoCommitSection = {
+ data: () => Store,
+
+ mixins: [RepoMixin],
+
+ computed: {
+ branchPaths() {
+ const branch = Helper.getBranch();
+ return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
+ },
+
+ cantCommitYet() {
+ return !this.commitMessage || this.submitCommitsLoading;
+ },
+
+ filePluralize() {
+ return this.changedFiles.length > 1 ? 'files' : 'file';
+ },
+ },
+
+ methods: {
+ makeCommit() {
+ // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
+ const branch = Helper.getBranch();
+ const commitMessage = this.commitMessage;
+ const actions = this.changedFiles.map(f => ({
+ action: 'update',
+ file_path: Helper.getFilePathFromFullPath(f.url, branch),
+ content: f.newContent,
+ }));
+ const payload = {
+ branch: Store.targetBranch,
+ commit_message: commitMessage,
+ actions,
+ };
+ Store.submitCommitsLoading = true;
+ Service.commitFiles(payload, this.resetCommitState);
+ },
+
+ resetCommitState() {
+ this.submitCommitsLoading = false;
+ this.changedFiles = [];
+ this.openedFiles = [];
+ this.commitMessage = '';
+ this.editMode = false;
+ $('html, body').animate({ scrollTop: 0 }, 'fast');
+ },
+ },
+};
+
+export default RepoCommitSection;
+</script>
+
+<template>
+<div id="commit-area" v-if="isCommitable && changedFiles.length" >
+ <form class="form-horizontal">
+ <fieldset>
+ <div class="form-group">
+ <label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
+ <div class="col-md-4">
+ <ul class="list-unstyled changed-files">
+ <li v-for="file in branchPaths" :key="file.id">
+ <span class="help-block">{{file}}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <!-- Textarea
+ -->
+ <div class="form-group">
+ <label class="col-md-4 control-label" for="commit-message">Commit message</label>
+ <div class="col-md-4">
+ <textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
+ </div>
+ </div>
+ <!-- Button Drop Down
+ -->
+ <div class="form-group target-branch">
+ <label class="col-md-4 control-label" for="target-branch">Target branch</label>
+ <div class="col-md-4">
+ <span class="help-block">{{targetBranch}}</span>
+ </div>
+ </div>
+ <div class="col-md-offset-4 col-md-4">
+ <button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
+ <i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
+ <span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
+ </button>
+ </div>
+ </fieldset>
+ </form>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
new file mode 100644
index 00000000000..e954fd38fc9
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -0,0 +1,49 @@
+<script>
+import Store from '../stores/repo_store';
+import RepoMixin from '../mixins/repo_mixin';
+
+export default {
+ data: () => Store,
+ mixins: [RepoMixin],
+ computed: {
+ buttonLabel() {
+ return this.editMode ? this.__('Cancel edit') : this.__('Edit');
+ },
+
+ buttonIcon() {
+ return this.editMode ? [] : ['fa', 'fa-pencil'];
+ },
+ },
+ methods: {
+ editClicked() {
+ if (this.changedFiles.length) {
+ this.dialog.open = true;
+ return;
+ }
+ this.editMode = !this.editMode;
+ Store.toggleBlobView();
+ },
+ },
+
+ watch: {
+ editMode() {
+ if (this.editMode) {
+ $('.project-refs-form').addClass('disabled');
+ $('.fa-long-arrow-right').show();
+ $('.project-refs-target-form').show();
+ } else {
+ $('.project-refs-form').removeClass('disabled');
+ $('.fa-long-arrow-right').hide();
+ $('.project-refs-target-form').hide();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
+ <i :class="buttonIcon"></i>
+ <span>{{buttonLabel}}</span>
+</button>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
new file mode 100644
index 00000000000..fd1a21e15b4
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -0,0 +1,135 @@
+<script>
+/* global monaco */
+import Store from '../stores/repo_store';
+import Service from '../services/repo_service';
+import Helper from '../helpers/repo_helper';
+
+const RepoEditor = {
+ data: () => Store,
+
+ destroyed() {
+ // this.monacoInstance.getModels().forEach((m) => {
+ // m.dispose();
+ // });
+ this.monacoInstance.destroy();
+ },
+
+ mounted() {
+ Service.getRaw(this.activeFile.raw_path)
+ .then((rawResponse) => {
+ Store.blobRaw = rawResponse.data;
+ Helper.findOpenedFileFromActive().plain = rawResponse.data;
+
+ const monacoInstance = this.monaco.editor.create(this.$el, {
+ model: null,
+ readOnly: false,
+ contextmenu: false,
+ });
+
+ Store.monacoInstance = monacoInstance;
+
+ this.addMonacoEvents();
+
+ const languages = this.monaco.languages.getLanguages();
+ const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
+ this.showHide();
+ const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
+
+ this.monacoInstance.setModel(newModel);
+ }).catch(Helper.loadingError);
+ },
+
+ methods: {
+ showHide() {
+ if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
+ this.$el.style.display = 'none';
+ } else {
+ this.$el.style.display = 'inline-block';
+ }
+ },
+
+ addMonacoEvents() {
+ this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
+ this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
+ },
+
+ onMonacoEditorKeysPressed() {
+ Store.setActiveFileContents(this.monacoInstance.getValue());
+ },
+
+ onMonacoEditorMouseUp(e) {
+ const lineNumber = e.target.position.lineNumber;
+ if (e.target.element.className === 'line-numbers') {
+ location.hash = `L${lineNumber}`;
+ Store.activeLine = lineNumber;
+ }
+ },
+ },
+
+ watch: {
+ activeLine() {
+ this.monacoInstance.setPosition({
+ lineNumber: this.activeLine,
+ column: 1,
+ });
+ },
+
+ activeFileLabel() {
+ this.showHide();
+ },
+
+ dialog: {
+ handler(obj) {
+ const newObj = obj;
+ if (newObj.status) {
+ newObj.status = false;
+ this.openedFiles.map((file) => {
+ const f = file;
+ if (f.active) {
+ this.blobRaw = f.plain;
+ }
+ f.changed = false;
+ delete f.newContent;
+
+ return f;
+ });
+ this.editMode = false;
+ }
+ },
+ deep: true,
+ },
+
+ isTree() {
+ this.showHide();
+ },
+
+ openedFiles() {
+ this.showHide();
+ },
+
+ binary() {
+ this.showHide();
+ },
+
+ blobRaw() {
+ this.showHide();
+
+ if (this.isTree) return;
+
+ this.monacoInstance.setModel(null);
+
+ const languages = this.monaco.languages.getLanguages();
+ const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
+ const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
+
+ this.monacoInstance.setModel(newModel);
+ },
+ },
+};
+
+export default RepoEditor;
+</script>
+
+<template>
+<div id="ide"></div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
new file mode 100644
index 00000000000..f604bc22a26
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -0,0 +1,66 @@
+<script>
+import TimeAgoMixin from '../../vue_shared/mixins/timeago';
+
+const RepoFile = {
+ mixins: [TimeAgoMixin],
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ isMini: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ loading: {
+ type: Object,
+ required: false,
+ default() { return { tree: false }; },
+ },
+ hasFiles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ activeFile: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ canShowFile() {
+ return !this.loading.tree || this.hasFiles;
+ },
+ },
+
+ methods: {
+ linkClicked(file) {
+ this.$emit('linkclicked', file);
+ },
+ },
+};
+
+export default RepoFile;
+</script>
+
+<template>
+<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
+ <td @click.prevent="linkClicked(file)">
+ <i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
+ <i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
+ <a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
+ </td>
+
+ <td v-if="!isMini" class="hidden-sm hidden-xs">
+ <div class="commit-message">
+ <a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
+ </div>
+ </td>
+
+ <td v-if="!isMini" class="hidden-xs">
+ <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
+ </td>
+</tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
new file mode 100644
index 00000000000..628d02ca704
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -0,0 +1,42 @@
+<script>
+import Store from '../stores/repo_store';
+import Helper from '../helpers/repo_helper';
+import RepoMixin from '../mixins/repo_mixin';
+
+const RepoFileButtons = {
+ data: () => Store,
+
+ mixins: [RepoMixin],
+
+ computed: {
+
+ rawDownloadButtonLabel() {
+ return this.binary ? 'Download' : 'Raw';
+ },
+
+ canPreview() {
+ return Helper.isKindaBinary();
+ },
+ },
+
+ methods: {
+ rawPreviewToggle: Store.toggleRawPreview,
+ },
+};
+
+export default RepoFileButtons;
+</script>
+
+<template>
+<div id="repo-file-buttons" v-if="isMini">
+ <a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
+
+ <div class="btn-group" role="group" aria-label="File actions">
+ <a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
+ <a :href="activeFile.commits_path" class="btn btn-default history">History</a>
+ <a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
+ </div>
+
+ <a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue
new file mode 100644
index 00000000000..ba53ce0eecc
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_file_options.vue
@@ -0,0 +1,25 @@
+<script>
+const RepoFileOptions = {
+ props: {
+ isMini: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+
+export default RepoFileOptions;
+</script>
+
+<template>
+<tr v-if="isMini" class="repo-file-options">
+ <td>
+ <span class="title">{{projectName}}</span>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
new file mode 100644
index 00000000000..38e9f16d041
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -0,0 +1,51 @@
+<script>
+const RepoLoadingFile = {
+ props: {
+ loading: {
+ type: Object,
+ required: false,
+ default: {},
+ },
+ hasFiles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isMini: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ methods: {
+ lineOfCode(n) {
+ return `line-of-code-${n}`;
+ },
+ },
+};
+
+export default RepoLoadingFile;
+</script>
+
+<template>
+<tr v-if="loading.tree && !hasFiles" class="loading-file">
+ <td>
+ <div class="animation-container animation-container-small">
+ <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
+ </div>
+ </td>
+
+ <td v-if="!isMini" class="hidden-sm hidden-xs">
+ <div class="animation-container">
+ <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
+ </div>
+ </td>
+
+ <td v-if="!isMini" class="hidden-xs">
+ <div class="animation-container animation-container-small">
+ <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
+ </div>
+ </td>
+</tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue
new file mode 100644
index 00000000000..6a0d684052f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue
@@ -0,0 +1,26 @@
+<script>
+const RepoPreviousDirectory = {
+ props: {
+ prevUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ methods: {
+ linkClicked(file) {
+ this.$emit('linkclicked', file);
+ },
+ },
+};
+
+export default RepoPreviousDirectory;
+</script>
+
+<template>
+<tr class="prev-directory">
+ <td colspan="3">
+ <a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
+ </td>
+</tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
new file mode 100644
index 00000000000..d8de022335b
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -0,0 +1,32 @@
+<script>
+import Store from '../stores/repo_store';
+
+export default {
+ data: () => Store,
+ mounted() {
+ $(this.$el).find('.file-content').syntaxHighlight();
+ },
+ computed: {
+ html() {
+ return this.activeFile.html;
+ },
+ },
+
+ watch: {
+ html() {
+ this.$nextTick(() => {
+ $(this.$el).find('.file-content').syntaxHighlight();
+ });
+ },
+ },
+};
+</script>
+
+<template>
+<div>
+ <div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
+ <div v-if="activeFile.render_error" class="vertical-center render-error">
+ <p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
new file mode 100644
index 00000000000..d6d832efc49
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -0,0 +1,104 @@
+<script>
+import Service from '../services/repo_service';
+import Helper from '../helpers/repo_helper';
+import Store from '../stores/repo_store';
+import RepoPreviousDirectory from './repo_prev_directory.vue';
+import RepoFileOptions from './repo_file_options.vue';
+import RepoFile from './repo_file.vue';
+import RepoLoadingFile from './repo_loading_file.vue';
+import RepoMixin from '../mixins/repo_mixin';
+
+const RepoSidebar = {
+ mixins: [RepoMixin],
+ components: {
+ 'repo-file-options': RepoFileOptions,
+ 'repo-previous-directory': RepoPreviousDirectory,
+ 'repo-file': RepoFile,
+ 'repo-loading-file': RepoLoadingFile,
+ },
+
+ created() {
+ this.addPopEventListener();
+ },
+
+ data: () => Store,
+
+ methods: {
+ addPopEventListener() {
+ window.addEventListener('popstate', () => {
+ if (location.href.indexOf('#') > -1) return;
+ this.linkClicked({
+ url: location.href,
+ });
+ });
+ },
+
+ linkClicked(clickedFile) {
+ let url = '';
+ let file = clickedFile;
+ if (typeof file === 'object') {
+ file.loading = true;
+ if (file.type === 'tree' && file.opened) {
+ file = Store.removeChildFilesOfTree(file);
+ file.loading = false;
+ } else {
+ url = file.url;
+ Service.url = url;
+ // I need to refactor this to do the `then` here.
+ // Not a callback. For now this is good enough.
+ // it works.
+ Helper.getContent(file, () => {
+ file.loading = false;
+ Helper.scrollTabsRight();
+ });
+ }
+ } else if (typeof file === 'string') {
+ // go back
+ url = file;
+ Service.url = url;
+ Helper.getContent(null, () => Helper.scrollTabsRight());
+ }
+ },
+ },
+};
+
+export default RepoSidebar;
+</script>
+
+<template>
+<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
+ <table class="table">
+ <thead v-if="!isMini">
+ <tr>
+ <th class="name">Name</th>
+ <th class="hidden-sm hidden-xs last-commit">Last Commit</th>
+ <th class="hidden-xs last-update">Last Update</th>
+ </tr>
+ </thead>
+ <tbody>
+ <repo-file-options
+ :is-mini="isMini"
+ :project-name="projectName"/>
+ <repo-previous-directory
+ v-if="isRoot"
+ :prev-url="prevURL"
+ @linkclicked="linkClicked(prevURL)"/>
+ <repo-loading-file
+ v-for="n in 5"
+ :key="n"
+ :loading="loading"
+ :has-files="!!files.length"
+ :is-mini="isMini"/>
+ <repo-file
+ v-for="file in files"
+ :key="file.id"
+ :file="file"
+ :is-mini="isMini"
+ @linkclicked="linkClicked(file)"
+ :is-tree="isTree"
+ :has-files="!!files.length"
+ :active-file="activeFile"/>
+ </tbody>
+ </table>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
new file mode 100644
index 00000000000..712d64c236f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -0,0 +1,45 @@
+<script>
+import Store from '../stores/repo_store';
+
+const RepoTab = {
+ props: {
+ tab: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ changedClass() {
+ const tabChangedObj = {
+ 'fa-times': !this.tab.changed,
+ 'fa-circle': this.tab.changed,
+ };
+ return tabChangedObj;
+ },
+ },
+
+ methods: {
+ tabClicked: Store.setActiveFiles,
+
+ xClicked(file) {
+ if (file.changed) return;
+ this.$emit('xclicked', file);
+ },
+ },
+};
+
+export default RepoTab;
+</script>
+
+<template>
+<li>
+ <a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
+ <i class="fa" :class="changedClass"></i>
+ </a>
+
+ <a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
+
+ <i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
+</li>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
new file mode 100644
index 00000000000..907a03e1601
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -0,0 +1,43 @@
+<script>
+import Vue from 'vue';
+import Store from '../stores/repo_store';
+import RepoTab from './repo_tab.vue';
+import RepoMixin from '../mixins/repo_mixin';
+
+const RepoTabs = {
+ mixins: [RepoMixin],
+
+ components: {
+ 'repo-tab': RepoTab,
+ },
+
+ data: () => Store,
+
+ methods: {
+ isOverflow() {
+ return this.$el.scrollWidth > this.$el.offsetWidth;
+ },
+
+ xClicked(file) {
+ Store.removeFromOpenedFiles(file);
+ },
+ },
+
+ watch: {
+ openedFiles() {
+ Vue.nextTick(() => {
+ this.tabsOverflow = this.isOverflow();
+ });
+ },
+ },
+};
+
+export default RepoTabs;
+</script>
+
+<template>
+<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
+ <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
+ <li class="tabs-divider" />
+</ul>
+</template>
diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
new file mode 100644
index 00000000000..8ee2df5c879
--- /dev/null
+++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
@@ -0,0 +1,21 @@
+/* global monaco */
+import RepoEditor from '../components/repo_editor.vue';
+import Store from '../stores/repo_store';
+import monacoLoader from '../monaco_loader';
+
+function repoEditorLoader() {
+ Store.monacoLoading = true;
+ return new Promise((resolve, reject) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ Store.monaco = monaco;
+ Store.monacoLoading = false;
+ resolve(RepoEditor);
+ }, reject);
+ });
+}
+
+const MonacoLoaderHelper = {
+ repoEditorLoader,
+};
+
+export default MonacoLoaderHelper;
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
new file mode 100644
index 00000000000..fee98c12592
--- /dev/null
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -0,0 +1,303 @@
+/* global Flash */
+import Service from '../services/repo_service';
+import Store from '../stores/repo_store';
+import '../../flash';
+
+const RepoHelper = {
+ getDefaultActiveFile() {
+ return {
+ active: true,
+ binary: false,
+ extension: '',
+ html: '',
+ mime_type: '',
+ name: '',
+ plain: '',
+ size: 0,
+ url: '',
+ raw: false,
+ newContent: '',
+ changed: false,
+ loading: false,
+ };
+ },
+
+ key: '',
+
+ isTree(data) {
+ return Object.hasOwnProperty.call(data, 'blobs');
+ },
+
+ Time: window.performance
+ && window.performance.now
+ ? window.performance
+ : Date,
+
+ getBranch() {
+ return $('button.dropdown-menu-toggle').attr('data-ref');
+ },
+
+ getLanguageIDForFile(file, langs) {
+ const ext = file.name.split('.').pop();
+ const foundLang = RepoHelper.findLanguage(ext, langs);
+
+ return foundLang ? foundLang.id : 'plaintext';
+ },
+
+ getFilePathFromFullPath(fullPath, branch) {
+ return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
+ },
+
+ findLanguage(ext, langs) {
+ return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
+ },
+
+ setDirectoryOpen(tree) {
+ const file = tree;
+ if (!file) return undefined;
+
+ file.opened = true;
+ file.icon = 'fa-folder-open';
+ RepoHelper.toURL(file.url, file.name);
+ return file;
+ },
+
+ isKindaBinary() {
+ const okExts = ['md', 'svg'];
+ return okExts.indexOf(Store.activeFile.extension) > -1;
+ },
+
+ setBinaryDataAsBase64(file) {
+ Service.getBase64Content(file.raw_path)
+ .then((response) => {
+ Store.blobRaw = response;
+ file.base64 = response; // eslint-disable-line no-param-reassign
+ })
+ .catch(RepoHelper.loadingError);
+ },
+
+ toggleFakeTab(loading, file) {
+ if (loading) return Store.addPlaceholderFile();
+ return Store.removeFromOpenedFiles(file);
+ },
+
+ setLoading(loading, file) {
+ if (Service.url.indexOf('blob') > -1) {
+ Store.loading.blob = loading;
+ return RepoHelper.toggleFakeTab(loading, file);
+ }
+
+ if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
+
+ return undefined;
+ },
+
+ getNewMergedList(inDirectory, currentList, newList) {
+ const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
+ if (!inDirectory) return newListSorted;
+ const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
+ if (!indexOfFile) return newListSorted;
+ return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
+ },
+
+ mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
+ newList.reverse().forEach((newFile) => {
+ const fileIndex = indexOfFile + 1;
+ const file = newFile;
+ file.level = inDirectory.level + 1;
+ oldList.splice(fileIndex, 0, file);
+ });
+
+ return oldList;
+ },
+
+ compareFilesCaseInsensitive(a, b) {
+ const aName = a.name.toLowerCase();
+ const bName = b.name.toLowerCase();
+ if (a.level > 0) return 0;
+ if (aName < bName) { return -1; }
+ if (aName > bName) { return 1; }
+ return 0;
+ },
+
+ isRoot(url) {
+ // the url we are requesting -> split by the project URL. Grab the right side.
+ const isRoot = !!url.split(Store.projectUrl)[1]
+ // remove the first "/"
+ .slice(1)
+ // split this by "/"
+ .split('/')
+ // remove the first two items of the array... usually /tree/master.
+ .slice(2)
+ // we want to know the length of the array.
+ // If greater than 0 not root.
+ .length;
+ return isRoot;
+ },
+
+ getContent(treeOrFile, cb) {
+ let file = treeOrFile;
+ // const loadingData = RepoHelper.setLoading(true);
+ return Service.getContent()
+ .then((response) => {
+ const data = response.data;
+ // RepoHelper.setLoading(false, loadingData);
+ if (cb) cb();
+ Store.isTree = RepoHelper.isTree(data);
+ if (!Store.isTree) {
+ if (!file) file = data;
+ Store.binary = data.binary;
+
+ if (data.binary) {
+ Store.binaryMimeType = data.mime_type;
+ // file might be undefined
+ RepoHelper.setBinaryDataAsBase64(data);
+ Store.setViewToPreview();
+ } else if (!Store.isPreviewView()) {
+ if (!data.render_error) {
+ Service.getRaw(data.raw_path)
+ .then((rawResponse) => {
+ Store.blobRaw = rawResponse.data;
+ data.plain = rawResponse.data;
+ RepoHelper.setFile(data, file);
+ }).catch(RepoHelper.loadingError);
+ }
+ }
+
+ if (Store.isPreviewView()) {
+ RepoHelper.setFile(data, file);
+ }
+
+ // if the file tree is empty
+ if (Store.files.length === 0) {
+ const parentURL = Service.blobURLtoParentTree(Service.url);
+ Service.url = parentURL;
+ RepoHelper.getContent();
+ }
+ } else {
+ // it's a tree
+ if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
+ file = RepoHelper.setDirectoryOpen(file);
+ const newDirectory = RepoHelper.dataToListOfFiles(data);
+ Store.addFilesToDirectory(file, Store.files, newDirectory);
+ Store.prevURL = Service.blobURLtoParentTree(Service.url);
+ }
+ }).catch(RepoHelper.loadingError);
+ },
+
+ setFile(data, file) {
+ const newFile = data;
+
+ newFile.url = file.url || location.pathname;
+ newFile.url = file.url;
+ if (newFile.render_error === 'too_large') {
+ newFile.tooLarge = true;
+ }
+ newFile.newContent = '';
+
+ Store.addToOpenedFiles(newFile);
+ Store.setActiveFiles(newFile);
+ },
+
+ toFA(icon) {
+ return `fa-${icon}`;
+ },
+
+ serializeBlob(blob) {
+ const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
+ simpleBlob.lastCommitMessage = blob.last_commit.message;
+ simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
+ simpleBlob.loading = false;
+
+ return simpleBlob;
+ },
+
+ serializeTree(tree) {
+ return RepoHelper.serializeRepoEntity('tree', tree);
+ },
+
+ serializeSubmodule(submodule) {
+ return RepoHelper.serializeRepoEntity('submodule', submodule);
+ },
+
+ serializeRepoEntity(type, entity) {
+ const { url, name, icon, last_commit } = entity;
+ const returnObj = {
+ type,
+ name,
+ url,
+ icon: RepoHelper.toFA(icon),
+ level: 0,
+ loading: false,
+ };
+
+ if (entity.last_commit) {
+ returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
+ } else {
+ returnObj.lastCommitUrl = '';
+ }
+ return returnObj;
+ },
+
+ scrollTabsRight() {
+ // wait for the transition. 0.1 seconds.
+ setTimeout(() => {
+ const tabs = document.getElementById('tabs');
+ if (!tabs) return;
+ tabs.scrollLeft = 12000;
+ }, 200);
+ },
+
+ dataToListOfFiles(data) {
+ const a = [];
+
+ // push in blobs
+ data.blobs.forEach((blob) => {
+ a.push(RepoHelper.serializeBlob(blob));
+ });
+
+ data.trees.forEach((tree) => {
+ a.push(RepoHelper.serializeTree(tree));
+ });
+
+ data.submodules.forEach((submodule) => {
+ a.push(RepoHelper.serializeSubmodule(submodule));
+ });
+
+ return a;
+ },
+
+ genKey() {
+ return RepoHelper.Time.now().toFixed(3);
+ },
+
+ getStateKey() {
+ return RepoHelper.key;
+ },
+
+ setStateKey(key) {
+ RepoHelper.key = key;
+ },
+
+ toURL(url, title) {
+ const history = window.history;
+
+ RepoHelper.key = RepoHelper.genKey();
+
+ history.pushState({ key: RepoHelper.key }, '', url);
+
+ if (title) {
+ document.title = `${title} · GitLab`;
+ }
+ },
+
+ findOpenedFileFromActive() {
+ return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
+ },
+
+ loadingError() {
+ Flash('Unable to load the file at this time.');
+ },
+};
+
+export default RepoHelper;
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
new file mode 100644
index 00000000000..67c03680fca
--- /dev/null
+++ b/app/assets/javascripts/repo/index.js
@@ -0,0 +1,74 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import Service from './services/repo_service';
+import Store from './stores/repo_store';
+import Repo from './components/repo.vue';
+import RepoEditButton from './components/repo_edit_button.vue';
+import Translate from '../vue_shared/translate';
+
+function initDropdowns() {
+ $('.project-refs-target-form').hide();
+ $('.fa-long-arrow-right').hide();
+}
+
+function addEventsForNonVueEls() {
+ $(document).on('change', '.dropdown', () => {
+ Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
+ });
+
+ window.onbeforeunload = function confirmUnload(e) {
+ const hasChanged = Store.openedFiles
+ .some(file => file.changed);
+ if (!hasChanged) return undefined;
+ const event = e || window.event;
+ if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
+ // For Safari
+ return 'Are you sure you want to lose unsaved changes?';
+ };
+}
+
+function setInitialStore(data) {
+ Store.service = Service;
+ Store.service.url = data.url;
+ Store.service.refsUrl = data.refsUrl;
+ Store.projectId = data.projectId;
+ Store.projectName = data.projectName;
+ Store.projectUrl = data.projectUrl;
+ Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
+ Store.checkIsCommitable();
+}
+
+function initRepo(el) {
+ return new Vue({
+ el,
+ components: {
+ repo: Repo,
+ },
+ });
+}
+
+function initRepoEditButton(el) {
+ return new Vue({
+ el,
+ components: {
+ repoEditButton: RepoEditButton,
+ },
+ });
+}
+
+function initRepoBundle() {
+ const repo = document.getElementById('repo');
+ const editButton = document.querySelector('.editable-mode');
+ setInitialStore(repo.dataset);
+ addEventsForNonVueEls();
+ initDropdowns();
+
+ Vue.use(Translate);
+
+ initRepo(repo);
+ initRepoEditButton(editButton);
+}
+
+$(initRepoBundle);
+
+export default initRepoBundle;
diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js
new file mode 100644
index 00000000000..c8e8238a0d3
--- /dev/null
+++ b/app/assets/javascripts/repo/mixins/repo_mixin.js
@@ -0,0 +1,17 @@
+import Store from '../stores/repo_store';
+
+const RepoMixin = {
+ computed: {
+ isMini() {
+ return !!Store.openedFiles.length;
+ },
+
+ changedFiles() {
+ const changedFileList = this.openedFiles
+ .filter(file => file.changed);
+ return changedFileList;
+ },
+ },
+};
+
+export default RepoMixin;
diff --git a/app/assets/javascripts/repo/monaco_loader.js b/app/assets/javascripts/repo/monaco_loader.js
new file mode 100644
index 00000000000..ad1370a7730
--- /dev/null
+++ b/app/assets/javascripts/repo/monaco_loader.js
@@ -0,0 +1,13 @@
+/* eslint-disable no-underscore-dangle, camelcase */
+/* global __webpack_public_path__ */
+
+import monacoContext from 'monaco-editor/dev/vs/loader';
+
+monacoContext.require.config({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`,
+ },
+});
+
+window.__monaco_context__ = monacoContext;
+export default monacoContext.require;
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
new file mode 100644
index 00000000000..8fba928e456
--- /dev/null
+++ b/app/assets/javascripts/repo/services/repo_service.js
@@ -0,0 +1,82 @@
+/* global Flash */
+import axios from 'axios';
+import Store from '../stores/repo_store';
+import Api from '../../api';
+
+const RepoService = {
+ url: '',
+ options: {
+ params: {
+ format: 'json',
+ },
+ },
+ richExtensionRegExp: /md/,
+
+ checkCurrentBranchIsCommitable() {
+ const url = Store.service.refsUrl;
+ return axios.get(url, { params: {
+ ref: Store.currentBranch,
+ search: Store.currentBranch,
+ } });
+ },
+
+ getRaw(url) {
+ return axios.get(url, {
+ transformResponse: [res => res],
+ });
+ },
+
+ buildParams(url = this.url) {
+ // shallow clone object without reference
+ const params = Object.assign({}, this.options.params);
+
+ if (this.urlIsRichBlob(url)) params.viewer = 'rich';
+
+ return params;
+ },
+
+ urlIsRichBlob(url = this.url) {
+ const extension = url.split('.').pop();
+
+ return this.richExtensionRegExp.test(extension);
+ },
+
+ getContent(url = this.url) {
+ const params = this.buildParams(url);
+
+ return axios.get(url, {
+ params,
+ });
+ },
+
+ getBase64Content(url = this.url) {
+ const request = axios.get(url, {
+ responseType: 'arraybuffer',
+ });
+
+ return request.then(response => this.bufferToBase64(response.data));
+ },
+
+ bufferToBase64(data) {
+ return new Buffer(data, 'binary').toString('base64');
+ },
+
+ blobURLtoParentTree(url) {
+ const urlArray = url.split('/');
+ urlArray.pop();
+ const blobIndex = urlArray.lastIndexOf('blob');
+
+ if (blobIndex > -1) urlArray[blobIndex] = 'tree';
+
+ return urlArray.join('/');
+ },
+
+ commitFiles(payload, cb) {
+ Api.commitMultiple(Store.projectId, payload, (data) => {
+ Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+ cb();
+ });
+ },
+};
+
+export default RepoService;
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
new file mode 100644
index 00000000000..06ca391ed0c
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/repo_store.js
@@ -0,0 +1,241 @@
+/* global Flash */
+import Helper from '../helpers/repo_helper';
+import Service from '../services/repo_service';
+
+const RepoStore = {
+ ideEl: {},
+ monaco: {},
+ monacoLoading: false,
+ monacoInstance: {},
+ service: '',
+ editor: '',
+ sidebar: '',
+ editMode: false,
+ isTree: false,
+ isRoot: false,
+ prevURL: '',
+ projectId: '',
+ projectName: '',
+ projectUrl: '',
+ trees: [],
+ blobs: [],
+ submodules: [],
+ blobRaw: '',
+ blobRendered: '',
+ currentBlobView: 'repo-preview',
+ openedFiles: [],
+ tabSize: 100,
+ defaultTabSize: 100,
+ minTabSize: 30,
+ tabsOverflow: 41,
+ submitCommitsLoading: false,
+ binaryLoaded: false,
+ dialog: {
+ open: false,
+ title: '',
+ status: false,
+ },
+ activeFile: Helper.getDefaultActiveFile(),
+ activeFileIndex: 0,
+ activeLine: 0,
+ activeFileLabel: 'Raw',
+ files: [],
+ isCommitable: false,
+ binary: false,
+ currentBranch: '',
+ targetBranch: 'new-branch',
+ commitMessage: '',
+ binaryMimeType: '',
+ // scroll bar space for windows
+ scrollWidth: 0,
+ binaryTypes: {
+ png: false,
+ md: false,
+ svg: false,
+ unknown: false,
+ },
+ loading: {
+ tree: false,
+ blob: false,
+ },
+ readOnly: true,
+
+ resetBinaryTypes() {
+ Object.keys(RepoStore.binaryTypes).forEach((key) => {
+ RepoStore.binaryTypes[key] = false;
+ });
+ },
+
+ // mutations
+ checkIsCommitable() {
+ RepoStore.service.checkCurrentBranchIsCommitable()
+ .then((data) => {
+ // you shouldn't be able to make commits on commits or tags.
+ const { Branches, Commits, Tags } = data.data;
+ if (Branches && Branches.length) RepoStore.isCommitable = true;
+ if (Commits && Commits.length) RepoStore.isCommitable = false;
+ if (Tags && Tags.length) RepoStore.isCommitable = false;
+ }).catch(() => Flash('Failed to check if branch can be committed to.'));
+ },
+
+ addFilesToDirectory(inDirectory, currentList, newList) {
+ RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
+ },
+
+ toggleRawPreview() {
+ RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
+ RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
+ },
+
+ setActiveFiles(file) {
+ if (RepoStore.isActiveFile(file)) return;
+ RepoStore.openedFiles = RepoStore.openedFiles
+ .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
+
+ RepoStore.setActiveToRaw();
+
+ if (file.binary) {
+ RepoStore.blobRaw = file.base64;
+ RepoStore.binaryMimeType = file.mime_type;
+ } else if (file.newContent || file.plain) {
+ RepoStore.blobRaw = file.newContent || file.plain;
+ } else {
+ Service.getRaw(file.raw_path)
+ .then((rawResponse) => {
+ RepoStore.blobRaw = rawResponse.data;
+ Helper.findOpenedFileFromActive().plain = rawResponse.data;
+ }).catch(Helper.loadingError);
+ }
+
+ if (!file.loading) Helper.toURL(file.url, file.name);
+ RepoStore.binary = file.binary;
+ },
+
+ setFileActivity(file, openedFile, i) {
+ const activeFile = openedFile;
+ activeFile.active = file.url === activeFile.url;
+
+ if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
+
+ return activeFile;
+ },
+
+ setActiveFile(activeFile, i) {
+ RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
+ RepoStore.activeFileIndex = i;
+ },
+
+ setActiveToRaw() {
+ RepoStore.activeFile.raw = false;
+ // can't get vue to listen to raw for some reason so RepoStore for now.
+ RepoStore.activeFileLabel = 'Display source';
+ },
+
+ removeChildFilesOfTree(tree) {
+ let foundTree = false;
+ const treeToClose = tree;
+ let wereDone = false;
+ RepoStore.files = RepoStore.files.filter((file) => {
+ const isItTheTreeWeWant = file.url === treeToClose.url;
+ // if it's the next tree
+ if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
+ wereDone = true;
+ return true;
+ }
+ if (wereDone) return true;
+
+ if (isItTheTreeWeWant) foundTree = true;
+
+ if (foundTree) return file.level <= treeToClose.level;
+ return true;
+ });
+
+ treeToClose.opened = false;
+ treeToClose.icon = 'fa-folder';
+ return treeToClose;
+ },
+
+ removeFromOpenedFiles(file) {
+ if (file.type === 'tree') return;
+ let foundIndex;
+ RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
+ if (openedFile.url === file.url) foundIndex = i;
+ return openedFile.url !== file.url;
+ });
+
+ // now activate the right tab based on what you closed.
+ if (RepoStore.openedFiles.length === 0) {
+ RepoStore.activeFile = {};
+ return;
+ }
+
+ if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
+ RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
+ return;
+ }
+
+ if (foundIndex) {
+ if (foundIndex > 0) {
+ RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
+ }
+ }
+ },
+
+ addPlaceholderFile() {
+ const randomURL = Helper.Time.now();
+ const newFakeFile = {
+ active: false,
+ binary: true,
+ type: 'blob',
+ loading: true,
+ mime_type: 'loading',
+ name: 'loading',
+ url: randomURL,
+ fake: true,
+ };
+
+ RepoStore.openedFiles.push(newFakeFile);
+
+ return newFakeFile;
+ },
+
+ addToOpenedFiles(file) {
+ const openFile = file;
+
+ const openedFilesAlreadyExists = RepoStore.openedFiles
+ .some(openedFile => openedFile.url === openFile.url);
+
+ if (openedFilesAlreadyExists) return;
+
+ openFile.changed = false;
+ RepoStore.openedFiles.push(openFile);
+ },
+
+ setActiveFileContents(contents) {
+ if (!RepoStore.editMode) return;
+ const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
+ RepoStore.activeFile.newContent = contents;
+ RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
+ currentFile.changed = RepoStore.activeFile.changed;
+ currentFile.newContent = contents;
+ },
+
+ toggleBlobView() {
+ RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
+ },
+
+ setViewToPreview() {
+ RepoStore.currentBlobView = 'repo-preview';
+ },
+
+ // getters
+
+ isActiveFile(file) {
+ return file && file.url === RepoStore.activeFile.url;
+ },
+
+ isPreviewView() {
+ return RepoStore.currentBlobView === 'repo-preview';
+ },
+};
+export default RepoStore;
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
new file mode 100644
index 00000000000..422c02c7b7e
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -0,0 +1,82 @@
+<script>
+/* global Flash */
+import editForm from './edit_form.vue';
+
+export default {
+ components: {
+ editForm,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
+ },
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
+ service: {
+ required: true,
+ type: Object,
+ },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ computed: {
+ faEye() {
+ const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
+ return {
+ [eye]: true,
+ };
+ },
+ },
+ methods: {
+ toggleForm() {
+ this.edit = !this.edit;
+ },
+ updateConfidentialAttribute(confidential) {
+ this.service.update('issue', { confidential })
+ .then(() => location.reload())
+ .catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue'));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block confidentiality">
+ <div class="sidebar-collapsed-icon">
+ <i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
+ </div>
+ <div class="title hide-collapsed">
+ Confidentiality
+ <a
+ v-if="isEditable"
+ class="pull-right confidential-edit"
+ href="#"
+ @click.prevent="toggleForm"
+ >
+ Edit
+ </a>
+ </div>
+ <div class="value confidential-value hide-collapsed">
+ <editForm
+ v-if="edit"
+ :toggle-form="toggleForm"
+ :is-confidential="isConfidential"
+ :update-confidential-attribute="updateConfidentialAttribute"
+ />
+ <div v-if="!isConfidential" class="no-value confidential-value">
+ <i class="fa fa-eye is-not-confidential"></i>
+ None
+ </div>
+ <div v-else class="value confidential-value hide-collapsed">
+ <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
+ This issue is confidential
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
new file mode 100644
index 00000000000..d578b663a54
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -0,0 +1,47 @@
+<script>
+import editFormButtons from './edit_form_buttons.vue';
+
+export default {
+ components: {
+ editFormButtons,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
+ },
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown open">
+ <div class="dropdown-menu confidential-warning-message">
+ <div>
+ <p v-if="!isConfidential">
+ You are going to turn on the confidentiality. This means that only team members with
+ <strong>at least Reporter access</strong>
+ are able to see and leave comments on the issue.
+ </p>
+ <p v-else>
+ You are going to turn off the confidentiality. This means
+ <strong>everyone</strong>
+ will be able to see and leave a comment on this issue.
+ </p>
+ <edit-form-buttons
+ :is-confidential="isConfidential"
+ :toggle-form="toggleForm"
+ :update-confidential-attribute="updateConfidentialAttribute"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
new file mode 100644
index 00000000000..97af4a3f505
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -0,0 +1,45 @@
+<script>
+export default {
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
+ },
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+ computed: {
+ onOrOff() {
+ return this.isConfidential ? 'Turn Off' : 'Turn On';
+ },
+ updateConfidentialBool() {
+ return !this.isConfidential;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="confidential-warning-message-actions">
+ <button
+ type="button"
+ class="btn btn-default append-right-10"
+ @click="toggleForm"
+ >
+ Cancel
+ </button>
+ <button
+ type="button"
+ class="btn btn-close"
+ @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+ >
+ {{ onOrOff }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index a9df66748c5..9edded3ead6 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees';
+import confidential from './components/confidential/confidential_issue_sidebar.vue';
import Mediator from './sidebar_mediator';
@@ -10,13 +11,28 @@ function domContentLoaded() {
mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
-
+ const confidentialEl = document.querySelector('#js-confidential-entry-point');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
}
+ if (confidentialEl) {
+ const dataNode = document.getElementById('js-confidential-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const ConfidentialComp = Vue.extend(confidential);
+
+ new ConfidentialComp({
+ propsData: {
+ isConfidential: initialData.is_confidential,
+ isEditable: initialData.is_editable,
+ service: mediator.service,
+ },
+ }).$mount(confidentialEl);
+ }
+
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
}
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
index ef401abce2d..8875590f0f2 100644
--- a/app/assets/javascripts/test_utils/index.js
+++ b/app/assets/javascripts/test_utils/index.js
@@ -1,3 +1,5 @@
+import 'core-js/es6/map';
+import 'core-js/es6/set';
import simulateDrag from './simulate_drag';
// Export to global space for rspec to use
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index 3dac31c2121..5e947769f8a 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -7,6 +7,14 @@ const LOADING_HTML = `
</div>
`;
+function getSystemDate(systemUtcOffsetSeconds) {
+ const date = new Date();
+ const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
+ const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
+ date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
+ return date;
+}
+
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
const dateDayName = gl.utils.getDayName(dateObject);
@@ -22,7 +30,7 @@ function formatTooltipText({ date, count }) {
const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath) {
+ constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
@@ -37,7 +45,7 @@ export default class ActivityCalendar {
this.timestampsTmp = [];
let group = 0;
- const today = new Date();
+ const today = getSystemDate(utcOffset);
today.setHours(0, 0, 0, 0, 0);
const oneYearAgo = new Date(today);
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js
index 5fe6603ce7b..1215b265e28 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/users/user_tabs.js
@@ -150,15 +150,21 @@ export default class UserTabs {
const $calendarWrap = this.$parentEl.find('.user-calendar');
const calendarPath = $calendarWrap.data('calendarPath');
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
+ const utcOffset = $calendarWrap.data('utcOffset');
+ let utcFormatted = 'UTC';
+ if (utcOffset !== 0) {
+ utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
+ }
$.ajax({
dataType: 'json',
url: calendarPath,
success: (activityData) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
+ $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
// eslint-disable-next-line no-new
- new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath);
+ new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset);
},
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
index a01cb8cc202..982b5e8e373 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
@@ -1,3 +1,5 @@
+import tooltip from '../../vue_shared/directives/tooltip';
+
export default {
name: 'MRWidgetAuthor',
props: {
@@ -5,11 +7,14 @@ export default {
showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false },
},
+ directives: {
+ tooltip,
+ },
template: `
<a
:href="author.webUrl || author.web_url"
- class="author-link"
- :class="{ 'has-tooltip': showAuthorTooltip }"
+ class="author-link inline"
+ :v-tooltip="showAuthorTooltip"
:title="author.name">
<img
:src="author.avatarUrl || author.avatar_url"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index 744a1cd24fa..e98d147733c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,8 +1,8 @@
/* global Flash */
import '~/lib/utils/datetime_utility';
-import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage';
+import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service';
export default {
@@ -13,11 +13,7 @@ export default {
},
components: {
'mr-widget-memory-usage': MemoryUsage,
- },
- computed: {
- svg() {
- return statusIconEntityMap.icon_status_success;
- },
+ 'status-icon': StatusIcon,
},
methods: {
formatDate(date) {
@@ -51,51 +47,51 @@ export default {
},
},
template: `
- <div class="mr-widget-heading">
+ <div class="mr-widget-heading deploy-heading">
<div v-for="deployment in mr.deployments">
- <div class="ci-widget">
+ <div class="ci-widget media">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
- <span class="ci-status-icon"
- v-html="svg"
- aria-hidden="true"></span>
+ <status-icon status="success" />
</span>
</div>
- <span>
- <span
- v-if="hasDeploymentMeta(deployment)">
- Deployed to
- </span>
- <a
- v-if="hasDeploymentMeta(deployment)"
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-meta">
- {{deployment.name}}
- </a>
- <span
- v-if="hasExternalUrls(deployment)">
- on
- </span>
- <a
- v-if="hasExternalUrls(deployment)"
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url">
- <i
- class="fa fa-external-link"
- aria-hidden="true" />
- {{deployment.external_url_formatted}}
- </a>
- <span
- v-if="hasDeploymentTime(deployment)"
- :data-title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- data-toggle="tooltip"
- data-placement="top">
- {{formatDate(deployment.deployed_at)}}
+ <div class="media-body space-children">
+ <span>
+ <span
+ v-if="hasDeploymentMeta(deployment)">
+ Deployed to
+ </span>
+ <a
+ v-if="hasDeploymentMeta(deployment)"
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta inline">
+ {{deployment.name}}
+ </a>
+ <span
+ v-if="hasExternalUrls(deployment)">
+ on
+ </span>
+ <a
+ v-if="hasExternalUrls(deployment)"
+ :href="deployment.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url inline">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true" />
+ {{deployment.external_url_formatted}}
+ </a>
+ <span
+ v-if="hasDeploymentTime(deployment)"
+ :data-title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
+ data-toggle="tooltip"
+ data-placement="top">
+ {{formatDate(deployment.deployed_at)}}
+ </span>
</span>
<button
type="button"
@@ -104,13 +100,13 @@ export default {
class="btn btn-default btn-xs">
Stop environment
</button>
- </span>
+ <mr-widget-memory-usage
+ v-if="deployment.metrics_url"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
</div>
- <mr-widget-memory-usage
- v-if="deployment.metrics_url"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 8430548903c..c05a76a3b4a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -1,3 +1,4 @@
+import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility';
export default {
@@ -5,6 +6,9 @@ export default {
props: {
mr: { type: Object, required: true },
},
+ directives: {
+ tooltip,
+ },
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
@@ -29,18 +33,51 @@ export default {
},
template: `
<div class="mr-source-target">
- <div
- v-if="mr.isOpen"
- class="pull-right">
+ <div class="normal">
+ <strong>
+ Request to merge
+ <span
+ class="label-branch"
+ :class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
+ :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
+ data-placement="bottom"
+ :v-tooltip="isBranchTitleLong(mr.sourceBranch)"
+ v-html="mr.sourceBranchLink"></span>
+ <button
+ v-tooltip
+ class="btn btn-transparent btn-clipboard"
+ data-title="Copy branch name to clipboard"
+ :data-clipboard-text="branchNameClipboardData">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard"></i>
+ </button>
+ into
+ <span
+ class="label-branch"
+ :v-tooltip="isBranchTitleLong(mr.sourceBranch)"
+ :class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
+ :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
+ data-placement="bottom">
+ <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
+ </span>
+ </strong>
+ <span
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count">
+ (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
+ </span>
+ </div>
+ <div v-if="mr.isOpen">
<a
href="#modal_merge_info"
data-toggle="modal"
- class="btn inline btn-grouped btn-sm">
+ class="btn btn-small inline">
Check out branch
</a>
- <span class="dropdown inline prepend-left-5">
+ <span class="dropdown inline prepend-left-10">
<a
- class="btn btn-sm dropdown-toggle"
+ class="btn btn-xs dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
@@ -69,38 +106,6 @@ export default {
</ul>
</span>
</div>
- <div class="normal">
- <strong>
- Request to merge
- <span
- class="label-branch"
- :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
- :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
- data-placement="bottom"
- v-html="mr.sourceBranchLink"></span>
- <button
- class="btn btn-transparent btn-clipboard has-tooltip"
- data-title="Copy branch name to clipboard"
- :data-clipboard-text="branchNameClipboardData">
- <i
- aria-hidden="true"
- class="fa fa-clipboard"></i>
- </button>
- into
- <span
- class="label-branch"
- :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
- :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
- data-placement="bottom">
- <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
- </span>
- </strong>
- <span
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count">
- (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
- </span>
- </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index 534e2a88eff..a4e34116c33 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -120,13 +120,12 @@ export default {
},
template: `
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
- <div class="legend"></div>
<p
v-if="shouldShowLoading"
class="usage-info js-usage-info usage-info-loading">
<i
class="fa fa-spinner fa-spin usage-info-load-spinner"
- aria-hidden="true" />Loading deployment statistics.
+ aria-hidden="true" />Loading deployment statistics
</p>
<p
v-if="shouldShowMemoryGraph"
@@ -136,12 +135,12 @@ export default {
<p
v-if="shouldShowLoadFailure"
class="usage-info js-usage-info usage-info-failed">
- Failed to load deployment statistics.
+ Failed to load deployment statistics
</p>
<p
v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable">
- Deployment statistics are not available currently.
+ Deployment statistics are not available currently
</p>
<mr-memory-graph
v-if="shouldShowMemoryGraph"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
index 2fecebce7a0..1d9f9863dd9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
@@ -16,7 +16,7 @@ export default {
<a
data-toggle="modal"
href="#modal_merge_info">
- command line.
+ command line
</a>
</section>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
index c02e10128e2..6c2e9ba1d30 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -29,58 +29,55 @@ export default {
},
template: `
<div class="mr-widget-heading">
- <div class="ci-widget">
+ <div class="ci-widget media">
<template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
- <span class="js-icon-link icon-link">
- <span
- v-html="svg"
- aria-hidden="true"></span>
- </span>
+ <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
+ <span
+ v-html="svg"
+ aria-hidden="true"></span>
+ </div>
+ <div class="media-body">
+ Could not connect to the CI server. Please check your settings and try again
</div>
- <span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
- <div>
+ <div class="ci-status-icon append-right-10">
<a
class="icon-link"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
- <span>
- Pipeline
- <a
- :href="mr.pipeline.path"
- class="pipeline-id">#{{mr.pipeline.id}}</a>
- {{mr.pipeline.details.status.label}}
- </span>
- <span
- v-if="mr.pipeline.details.stages.length > 0">
- with {{stageText}}
- </span>
- <div class="mr-widget-pipeline-graph">
- <div class="stage-cell">
- <div
- v-if="mr.pipeline.details.stages.length > 0"
- v-for="stage in mr.pipeline.details.stages"
- class="stage-container dropdown js-mini-pipeline-graph">
- <pipeline-stage :stage="stage" />
- </div>
- </div>
+ <div class="media-body">
+ <span>
+ Pipeline
+ <a
+ :href="mr.pipeline.path"
+ class="pipeline-id">#{{mr.pipeline.id}}</a>
+ </span>
+ <span class="mr-widget-pipeline-graph">
+ <span class="stage-cell">
+ <div
+ v-if="mr.pipeline.details.stages.length > 0"
+ v-for="stage in mr.pipeline.details.stages"
+ class="stage-container dropdown js-mini-pipeline-graph">
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
+ </span>
+ <span>
+ {{mr.pipeline.details.status.label}} for
+ <a
+ :href="mr.pipeline.commit.commit_path"
+ class="commit-sha js-commit-link">
+ {{mr.pipeline.commit.short_id}}</a>.
+ </span>
+ <span
+ v-if="mr.pipeline.coverage"
+ class="js-mr-coverage">
+ Coverage {{mr.pipeline.coverage}}%
+ </span>
</div>
- <span>
- for
- <a
- :href="mr.pipeline.commit.commit_path"
- class="commit-sha js-commit-link">
- {{mr.pipeline.commit.short_id}}</a>.
- </span>
- <span
- v-if="mr.pipeline.coverage"
- class="js-mr-coverage">
- Coverage {{mr.pipeline.coverage}}%.
- </span>
</template>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
index 205804670fa..563267ad044 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
@@ -2,37 +2,32 @@ export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: { type: Object, required: true },
+ state: { type: String, required: false },
},
computed: {
hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe;
},
- },
- methods: {
- hasMultipleIssues(text) {
- return !text ? false : text.match(/<\/a> and <a/);
- },
- issueLabel(field) {
- return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
- },
- verbLabel(field) {
- return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
+ closesText() {
+ if (this.state === 'merged') {
+ return 'Closed';
+ }
+ if (this.state === 'closed') {
+ return 'Did not close';
+ }
+ return 'Closes';
},
},
template: `
<section
v-if="hasLinks"
class="mr-info-list mr-links">
- <div class="legend"></div>
<p v-if="relatedLinks.closing">
- Closes {{issueLabel('closing')}}
- <span v-html="relatedLinks.closing"></span>.
+ {{closesText}} <span v-html="relatedLinks.closing"></span>
</p>
<p v-if="relatedLinks.mentioned">
- <span class="capitalize">{{issueLabel('mentioned')}}</span>
- <span v-html="relatedLinks.mentioned"></span>
- {{verbLabel('mentioned')}} mentioned but will not be closed.
+ Mentions <span v-html="relatedLinks.mentioned"></span>
</p>
<p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
new file mode 100644
index 00000000000..b01c923311b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -0,0 +1,36 @@
+import ciIcon from '../../vue_shared/components/ci_icon.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ props: {
+ status: { type: String, required: true },
+ showDisabledButton: { type: Boolean, required: false },
+ },
+ components: {
+ ciIcon,
+ loadingIcon,
+ },
+ computed: {
+ statusObj() {
+ return {
+ group: this.status,
+ icon: `icon_status_${this.status}`,
+ };
+ },
+ },
+ template: `
+ <div class="space-children flex-container-block append-right-10">
+ <div v-if="status === 'loading'" class="mr-widget-icon">
+ <loading-icon />
+ </div>
+ <ci-icon v-else :status="statusObj" />
+ <button
+ v-if="showDisabledButton"
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
index c7f25a1697c..2b16a2d6817 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
@@ -1,16 +1,26 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetArchived',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- This project is archived, write access has been disabled.
- </span>
+ <div class="mr-widget-body media">
+ <div class="space-children">
+ <status-icon status="failed" />
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ </div>
+ <div class="media-body">
+ <span class="bold">
+ This project is archived, write access has been disabled
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
index 4063859d5d0..5648208f7b1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
@@ -1,4 +1,5 @@
import eventHub from '../../event_hub';
+import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetAutoMergeFailed',
@@ -10,6 +11,9 @@ export default {
isRefreshing: false,
};
},
+ components: {
+ statusIcon,
+ },
methods: {
refreshWidget() {
this.isRefreshing = true;
@@ -19,18 +23,16 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <button
- class="btn btn-success btn-small"
- disabled="true"
- type="button">
- Merge
- </button>
- <span class="bold danger">
- This merge request failed to be merged automatically.
+ <div class="mr-widget-body media">
+ <status-icon status="failed" />
+ <div class="media-body space-children">
+ <span class="bold">
+ <template v-if="mr.mergeError">{{mr.mergeError}}.</template>
+ This merge request failed to be merged automatically
+ </span>
<button
@click="refreshWidget"
- :class="{ disabled: isRefreshing }"
+ :disabled="isRefreshing"
type="button"
class="btn btn-xs btn-default">
<i
@@ -39,9 +41,6 @@ export default {
aria-hidden="true" />
Refresh
</button>
- </span>
- <div class="merge-error-text danger bold">
- {{mr.mergeError}}
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
index 8515b54e62d..aaf9d3304a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -1,19 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetChecking',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- Checking ability to merge automatically.
- <i
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="loading" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ Checking ability to merge automatically
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index fc2e42c6821..4078aad7f83 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -1,4 +1,5 @@
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetClosed',
@@ -7,24 +8,28 @@ export default {
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
+ statusIcon,
},
template: `
- <div class="mr-widget-body">
- <mr-widget-author-and-time
- actionText="Closed by"
- :author="mr.closedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.closedAt"
- />
- <section>
- <p>
- The changes were not merged into
- <a
- :href="mr.targetBranchPath"
- class="label-branch">
- {{mr.targetBranch}}</a>.
- </p>
- </section>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" />
+ <div class="media-body">
+ <mr-widget-author-and-time
+ actionText="Closed by"
+ :author="mr.closedBy"
+ :dateTitle="mr.updatedAt"
+ :dateReadable="mr.closedAt"
+ />
+ <section class="mr-info-list">
+ <p>
+ The changes were not merged into
+ <a
+ :href="mr.targetBranchPath"
+ class="label-branch">
+ {{mr.targetBranch}}</a>
+ </p>
+ </section>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
index 36596c6f37e..f9cb79a0bc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -1,27 +1,25 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetConflicts',
props: {
mr: { type: Object, required: true },
},
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- There are merge conflicts.
- <span v-if="!mr.canMerge">
- Resolve these conflicts or ask someone with write access to this repository to merge it locally.
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ There are merge conflicts<span v-if="!mr.canMerge">.</span>
+ <span v-if="!mr.canMerge">
+ Resolve these conflicts or ask someone with write access to this repository to merge it locally
+ </span>
</span>
- </span>
- <div
- v-if="mr.canMerge"
- class="btn-group">
<a
- v-if="mr.conflictResolutionPath"
+ v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
class="btn btn-default btn-xs js-resolve-conflicts-button">
Resolve conflicts
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index 600b4d42e3d..1cb24549d53 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -1,3 +1,4 @@
+import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
export default {
@@ -38,39 +39,40 @@ export default {
}
},
},
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- class="btn btn-success btn-small"
- disabled="true"
- type="button">
- Merge
- </button>
- <span
- v-if="!isRefreshing"
- class="bold danger">
- <span
- class="has-error-message"
- v-if="mr.mergeError">
- {{mr.mergeError}}
- </span>
- <span v-else>Merge failed.</span>
- <span
- :class="{ 'has-custom-error': mr.mergeError }">
- Refreshing in {{timerText}} to show the updated status...
+ <div class="mr-widget-body media">
+ <template v-if="isRefreshing">
+ <status-icon status="loading" />
+ <span class="media-body bold js-refresh-label">
+ Refreshing now
</span>
- <button
- @click="refresh"
- class="btn btn-default btn-xs js-refresh-button"
- type="button">
- Refresh now
- </button>
- </span>
- <span
- v-if="isRefreshing"
- class="bold js-refresh-label">
- Refreshing now...
- </span>
+ </template>
+ <template v-else>
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ <span
+ class="has-error-message"
+ v-if="mr.mergeError">
+ {{mr.mergeError}}.
+ </span>
+ <span v-else>Merge failed.</span>
+ <span
+ :class="{ 'has-custom-error': mr.mergeError }">
+ Refreshing in {{timerText}} to show the updated status...
+ </span>
+ </span>
+ <button
+ @click="refresh"
+ class="btn btn-default btn-xs js-refresh-button"
+ type="button">
+ Refresh now
+ </button>
+ </div>
+ </template>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
deleted file mode 100644
index 0bd31731a0b..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
+++ /dev/null
@@ -1,24 +0,0 @@
-export default {
- name: 'MRWidgetLocked',
- props: {
- mr: { type: Object, required: true },
- },
- template: `
- <div class="mr-widget-body mr-state-locked">
- <span class="state-label">Locked</span>
- This merge request is in the process of being merged, during which time it is locked and cannot be closed.
- <i
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- <section class="mr-info-list mr-links">
- <div class="legend"></div>
- <p>
- The changes will be merged into
- <span class="label-branch">
- <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
- </span>.
- </p>
- </section>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index 419d174f3ff..bdfd4d9667c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -1,5 +1,5 @@
/* global Flash */
-
+import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
@@ -11,6 +11,7 @@ export default {
},
components: {
'mr-widget-author': MRWidgetAuthor,
+ statusIcon,
},
data() {
return {
@@ -61,56 +62,56 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <h4>
- Set by
- <mr-widget-author :author="mr.setToMWPSBy" />
- to be merged automatically when the pipeline succeeds.
- <a
- v-if="mr.canCancelAutomaticMerge"
- @click.prevent="cancelAutomaticMerge"
- :disabled="isCancellingAutoMerge"
- role="button"
- href="#"
- class="btn btn-xs btn-default js-cancel-auto-merge">
- <i
- v-if="isCancellingAutoMerge"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Cancel automatic merge
- </a>
- </h4>
- <section class="mr-info-list">
- <div class="legend"></div>
- <p>The changes will be merged into
- <a
- :href="mr.targetBranchPath"
- class="label-branch">
- {{mr.targetBranch}}
- </a>.
- </p>
- <p v-if="mr.shouldRemoveSourceBranch">
- The source branch will be removed.
- </p>
- <p
- v-else
- class="with-button">
- The source branch will not be removed.
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <div class="media-body">
+ <h4>
+ Set by
+ <mr-widget-author :author="mr.setToMWPSBy" />
+ to be merged automatically when the pipeline succeeds
<a
- v-if="canRemoveSourceBranch"
- :disabled="isRemovingSourceBranch"
- @click.prevent="removeSourceBranch"
+ v-if="mr.canCancelAutomaticMerge"
+ @click.prevent="cancelAutomaticMerge"
+ :disabled="isCancellingAutoMerge"
role="button"
- class="btn btn-xs btn-default js-remove-source-branch"
- href="#">
+ href="#"
+ class="btn btn-xs btn-default js-cancel-auto-merge">
<i
- v-if="isRemovingSourceBranch"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Remove source branch
+ v-if="isCancellingAutoMerge"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Cancel automatic merge
</a>
- </p>
- </section>
+ </h4>
+ <section class="mr-info-list">
+ <p>The changes will be merged into
+ <a
+ :href="mr.targetBranchPath"
+ class="label-branch">
+ {{mr.targetBranch}}
+ </a>
+ </p>
+ <p v-if="mr.shouldRemoveSourceBranch">
+ The source branch will be removed
+ </p>
+ <p v-else>
+ The source branch will not be removed
+ <a
+ v-if="canRemoveSourceBranch"
+ :disabled="isRemovingSourceBranch"
+ @click.prevent="removeSourceBranch"
+ role="button"
+ class="btn btn-xs btn-default js-remove-source-branch"
+ href="#">
+ <i
+ v-if="isRemovingSourceBranch"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Remove source branch
+ </a>
+ </p>
+ </section>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index c7d32d18141..e452260a4d0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -1,6 +1,9 @@
/* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
export default {
@@ -9,14 +12,19 @@ export default {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
- components: {
- 'mr-widget-author-and-time': mrWidgetAuthorTime,
- },
data() {
return {
isMakingRequest: false,
};
},
+ directives: {
+ tooltip,
+ },
+ components: {
+ 'mr-widget-author-and-time': mrWidgetAuthorTime,
+ loadingIcon,
+ statusIcon,
+ },
computed: {
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
@@ -55,75 +63,77 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <mr-widget-author-and-time
- actionText="Merged by"
- :author="mr.mergedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.mergedAt" />
- <section class="mr-info-list">
- <div class="legend"></div>
- <p>
- The changes were merged into
- <span class="label-branch">
- <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
- </span>
- </p>
- <p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p>
- <p v-if="shouldShowRemoveSourceBranch">
- You can remove source branch now.
- <button
- @click="removeSourceBranch"
- :class="{ disabled: isMakingRequest }"
- type="button"
- class="btn btn-xs btn-default js-remove-branch-button">
- Remove Source Branch
- </button>
- </p>
- <p v-if="shouldShowSourceBranchRemoving">
- <i
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- The source branch is being removed.
- </p>
- </section>
- <div
- v-if="shouldShowMergedButtons"
- class="merged-buttons clearfix">
- <a
- v-if="mr.canRevertInCurrentMR"
- class="btn btn-close btn-sm has-tooltip"
- href="#modal-revert-commit"
- data-toggle="modal"
- data-container="body"
- title="Revert this merge request in a new merge request">
- Revert
- </a>
- <a
- v-else-if="mr.revertInForkPath"
- class="btn btn-close btn-sm has-tooltip"
- data-method="post"
- :href="mr.revertInForkPath"
- title="Revert this merge request in a new merge request">
- Revert
- </a>
- <a
- v-if="mr.canCherryPickInCurrentMR"
- class="btn btn-default btn-sm has-tooltip"
- href="#modal-cherry-pick-commit"
- data-toggle="modal"
- data-container="body"
- title="Cherry-pick this merge request in a new merge request">
- Cherry-pick
- </a>
- <a
- v-else-if="mr.cherryPickInForkPath"
- class="btn btn-default btn-sm has-tooltip"
- data-method="post"
- :href="mr.cherryPickInForkPath"
- title="Cherry-pick this merge request in a new merge request">
- Cherry-pick
- </a>
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <div class="media-body">
+ <div class="space-children">
+ <mr-widget-author-and-time
+ actionText="Merged by"
+ :author="mr.mergedBy"
+ :dateTitle="mr.updatedAt"
+ :dateReadable="mr.mergedAt" />
+ <a
+ v-if="mr.canRevertInCurrentMR"
+ v-tooltip
+ class="btn btn-close btn-xs"
+ href="#modal-revert-commit"
+ data-toggle="modal"
+ data-container="body"
+ title="Revert this merge request in a new merge request">
+ Revert
+ </a>
+ <a
+ v-else-if="mr.revertInForkPath"
+ v-tooltip
+ class="btn btn-close btn-xs"
+ data-method="post"
+ :href="mr.revertInForkPath"
+ title="Revert this merge request in a new merge request">
+ Revert
+ </a>
+ <a
+ v-if="mr.canCherryPickInCurrentMR"
+ v-tooltip
+ class="btn btn-default btn-xs"
+ href="#modal-cherry-pick-commit"
+ data-toggle="modal"
+ data-container="body"
+ title="Cherry-pick this merge request in a new merge request">
+ Cherry-pick
+ </a>
+ <a
+ v-else-if="mr.cherryPickInForkPath"
+ v-tooltip
+ class="btn btn-default btn-xs"
+ data-method="post"
+ :href="mr.cherryPickInForkPath"
+ title="Cherry-pick this merge request in a new merge request">
+ Cherry-pick
+ </a>
+ </div>
+ <section class="mr-info-list">
+ <p>
+ The changes were merged into
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </p>
+ <p v-if="mr.sourceBranchRemoved">The source branch has been removed</p>
+ <p v-if="shouldShowRemoveSourceBranch" class="space-children">
+ <span>You can remove source branch now</span>
+ <button
+ @click="removeSourceBranch"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-xs btn-default js-remove-branch-button">
+ Remove Source Branch
+ </button>
+ </p>
+ <p v-if="shouldShowSourceBranchRemoving">
+ <loading-icon inline />
+ <span>The source branch is being removed</span>
+ </p>
+ </section>
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js
new file mode 100644
index 00000000000..f6d1a4feeb2
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js
@@ -0,0 +1,29 @@
+import statusIcon from '../mr_widget_status_icon';
+
+export default {
+ name: 'MRWidgetMerging',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ components: {
+ statusIcon,
+ },
+ template: `
+ <div class="mr-widget-body mr-state-locked media">
+ <status-icon status="loading" />
+ <div class="media-body">
+ <h4>
+ This merge request is in the process of being merged
+ </h4>
+ <section class="mr-info-list">
+ <p>
+ The changes will be merged into
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </p>
+ </section>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 328382485f6..9f0a359d01a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -1,3 +1,5 @@
+import statusIcon from '../mr_widget_status_icon';
+import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
export default {
@@ -5,30 +7,37 @@ export default {
props: {
mr: { type: Object, required: true },
},
+ directives: {
+ tooltip,
+ },
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
+ statusIcon,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
+ message() {
+ return `If the ${this.missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line`;
+ },
},
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold js-branch-text">
- <span class="capitalize">
- {{missingBranchName}}
- </span> branch does not exist.
- Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch.
- </span>
- <mr-widget-merge-help
- :missing-branch="missingBranchName" />
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold js-branch-text">
+ <span class="capitalize">
+ {{missingBranchName}}
+ </span> branch does not exist.
+ Please restore it or use a different {{missingBranchName}} branch
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ :title="message"
+ :aria-label="message"></i>
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
index 07169b349be..797511d4e3a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -1,17 +1,19 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetNotAllowed',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- Ready to be merged automatically.
- Ask someone with write access to this repository to merge this request.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="success" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ Ready to be merged automatically.
+ Ask someone with write access to this repository to merge this request
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
index 375a382615a..ebfd6765934 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
@@ -12,7 +12,7 @@ export default {
return { emptyStateSVG };
},
template: `
- <div class="mr-widget-body empty-state">
+ <div class="mr-widget-body mr-widget-empty-state">
<div class="row">
<div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
<span v-html="emptyStateSVG"></span>
@@ -29,12 +29,14 @@ export default {
Currently there are no changes in this merge request's source branch.
Please push new commits or use a different branch.
</p>
- <a
- v-if="mr.newBlobPath"
- :href="mr.newBlobPath"
- class="btn btn-inverted btn-save">
- Create file
- </a>
+ <div>
+ <a
+ v-if="mr.newBlobPath"
+ :href="mr.newBlobPath"
+ class="btn btn-inverted btn-save">
+ Create file
+ </a>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 31c53b679ed..167a0d4613a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -1,16 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetPipelineBlocked',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index 002820123ca..c5be9a0530a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -1,16 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetPipelineBlocked',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- class="btn btn-success btn-small"
- disabled="true"
- type="button">
- Merge
- </button>
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index fcd4fdaf09f..65187754009 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -1,8 +1,8 @@
/* global Flash */
-
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
export default {
@@ -25,6 +25,9 @@ export default {
warningSvg,
};
},
+ components: {
+ statusIcon,
+ },
computed: {
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
@@ -196,84 +199,98 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <span class="btn-group">
- <button
- @click="handleMergeButtonClick()"
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- {{mergeButtonText}}
- </button>
- <button
- v-if="shouldShowMergeOptionsDropdown"
- :disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-small btn-info dropdown-toggle"
- data-toggle="dropdown">
- <i
- class="fa fa-caret-down"
- aria-hidden="true" />
- <span class="sr-only">
- Select merge moment
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <div class="media-body">
+ <div class="media space-children">
+ <span class="btn-group">
+ <button
+ @click="handleMergeButtonClick()"
+ :disabled="isMergeButtonDisabled"
+ :class="mergeButtonClass"
+ type="button">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ {{mergeButtonText}}
+ </button>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-small btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment">
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true" />
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu">
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(true)"
+ class="merge_when_pipeline_succeeds"
+ href="#">
+ <span class="media">
+ <span
+ v-html="successSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(false, true)"
+ class="accept-merge-request"
+ href="#">
+ <span class="media">
+ <span
+ v-html="warningSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge immediately</span>
+ </span>
+ </a>
+ </li>
+ </ul>
</span>
- </button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu">
- <li>
- <a
- @click.prevent="handleMergeButtonClick(true)"
- class="merge_when_pipeline_succeeds"
- href="#">
- <span
- v-html="successSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="merge-opt-title">Merge when pipeline succeeds</span>
- </a>
- </li>
- <li>
- <a
- @click.prevent="handleMergeButtonClick(false, true)"
- class="accept-merge-request"
- href="#">
- <span
- v-html="warningSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="merge-opt-title">Merge immediately</span>
- </a>
- </li>
- </ul>
- </span>
- <template v-if="isMergeAllowed()">
- <label class="spacing">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- type="checkbox"/> Remove source branch
- </label>
+ <div class="media-body space-children">
+ <template v-if="isMergeAllowed()">
+ <label>
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ type="checkbox"/> Remove source branch
+ </label>
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- :mr="mr"
- :is-merge-button-disabled="isMergeButtonDisabled" />
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ :mr="mr"
+ :is-merge-button-disabled="isMergeButtonDisabled" />
- <button
- @click="toggleCommitMessageEditor"
- :disabled="isMergeButtonDisabled"
- class="btn btn-default btn-xs"
- type="button">
- Modify commit message
- </button>
+ <button
+ @click="toggleCommitMessageEditor"
+ :disabled="isMergeButtonDisabled"
+ class="btn btn-default btn-xs"
+ type="button">
+ Modify commit message
+ </button>
+ </template>
+ <template v-else>
+ <span class="bold">
+ The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
+ </span>
+ </template>
+ </div>
+ </div>
<div
v-if="showCommitMessageEditor"
class="prepend-top-default commit-message-editor">
@@ -293,7 +310,7 @@ export default {
rows="14"
name="Commit message"></textarea>
</div>
- <p class="hint">Try to keep the first line under 52 characters and the others under 72.</p>
+ <p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
<div class="hint">
<a
@click.prevent="updateCommitMessage"
@@ -302,12 +319,7 @@ export default {
</div>
</div>
</div>
- </template>
- <template v-else>
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
- </span>
- </template>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index 79f8ef408e6..89f38e5bd2a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -1,16 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetSHAMismatch',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- The source branch HEAD has recently changed. Please reload the page and review the changes before merging.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ The source branch HEAD has recently changed. Please reload the page and review the changes before merging
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index f4ab2d9fa58..d762ca6e640 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -1,27 +1,27 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetUnresolvedDiscussions',
props: {
mr: { type: Object, required: true },
},
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- There are unresolved discussions. Please resolve these discussions
- <span v-if="mr.canCreateIssue">or</span>
- <span v-else>.</span>
- </span>
- <a
- v-if="mr.createIssueToResolveDiscussionsPath"
- :href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue">
- Create an issue to resolve them later
- </a>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ There are unresolved discussions. Please resolve these discussions
+ </span>
+ <a
+ v-if="mr.createIssueToResolveDiscussionsPath"
+ :href="mr.createIssueToResolveDiscussionsPath"
+ class="btn btn-default btn-xs js-create-issue">
+ Create an issue to resolve them later
+ </a>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index cb02ffe93bd..b11a06899cf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -1,4 +1,6 @@
/* global Flash */
+import statusIcon from '../mr_widget_status_icon';
+import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
export default {
@@ -7,11 +9,17 @@ export default {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
+ directives: {
+ tooltip,
+ },
data() {
return {
isMakingRequest: false,
};
},
+ components: {
+ statusIcon,
+ },
methods: {
removeWIP() {
this.isMakingRequest = true;
@@ -29,20 +37,20 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge</button>
- <span class="bold">
- This merge request is currently Work In Progress and therefore unable to merge
- </span>
- <template v-if="mr.removeWIPPath">
- <i
- class="fa fa-question-circle has-tooltip"
- title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged." />
+ <div class="mr-widget-body media">
+ <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
+ <div class="media-body space-children">
+ <span class="bold">
+ This is a Work in Progress
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
+ aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
+ </i>
+ </span>
<button
+ v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
@@ -53,7 +61,7 @@ export default {
aria-hidden="true" />
Resolve WIP status
</button>
- </template>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index fe5e1bbb55c..49340c232c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -1,7 +1,7 @@
/**
* This file is the centerpiece of an attempt to reduce potential conflicts
* between the CE and EE versions of the MR widget. EE additions to the MR widget should
- * be contained in the ./vue_merge_request_widget/ee directory, and should **extend**
+ * be contained in the ee/vue_merge_request_widget directory, and should **extend**
* rather than mutate CE MR Widget code.
*
* This file should be the only source of conflicts between EE and CE. EE-only components should
@@ -19,7 +19,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li
export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed';
-export { default as LockedState } from './components/states/mr_widget_locked';
+export { default as MergingState } from './components/states/mr_widget_merging';
export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived';
export { default as ConflictsState } from './components/states/mr_widget_conflicts';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 2339a00ddd0..0042c48816f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -8,7 +8,7 @@ import {
WidgetRelatedLinks,
MergedState,
ClosedState,
- LockedState,
+ MergingState,
WipState,
ArchivedState,
ConflictsState,
@@ -35,8 +35,14 @@ import {
export default {
el: '#js-vue-mr-widget',
name: 'MRWidget',
+ props: {
+ mrData: {
+ type: Object,
+ required: false,
+ },
+ },
data() {
- const store = new MRWidgetStore(gl.mrWidgetData);
+ const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
const service = this.createService(store);
return {
mr: store,
@@ -206,7 +212,7 @@ export default {
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState,
- 'mr-widget-locked': LockedState,
+ 'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState,
'mr-widget-archived': ArchivedState,
@@ -234,14 +240,21 @@ export default {
v-if="shouldRenderDeployments"
:mr="mr"
:service="service" />
- <component
- :is="componentName"
- :mr="mr"
- :service="service" />
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :related-links="mr.relatedLinks" />
- <mr-widget-merge-help v-if="shouldRenderMergeHelp" />
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service" />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks" />
+ </div>
+ <div
+ class="mr-widget-footer"
+ v-if="shouldRenderMergeHelp">
+ <mr-widget-merge-help />
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index fddafb0ddfa..fbea764b739 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
+ this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
@@ -94,6 +95,11 @@ export default class MergeRequestStore {
}
setState(data) {
+ if (this.mergeOngoing) {
+ this.state = 'merging';
+ return;
+ }
+
if (this.isOpen) {
this.state = getStateKey.call(this, data);
} else {
@@ -104,9 +110,6 @@ export default class MergeRequestStore {
case 'closed':
this.state = 'closed';
break;
- case 'locked':
- this.state = 'locked';
- break;
default:
this.state = null;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 605dd3a1ff4..9074a064a6d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -1,7 +1,7 @@
const stateToComponentMap = {
merged: 'mr-widget-merged',
closed: 'mr-widget-closed',
- locked: 'mr-widget-locked',
+ merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
workInProgress: 'mr-widget-wip',
@@ -20,7 +20,7 @@ const stateToComponentMap = {
};
const statesToShowHelpWidget = [
- 'locked',
+ 'merging',
'conflicts',
'workInProgress',
'readyToMerge',
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
new file mode 100644
index 00000000000..7d339c0e753
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -0,0 +1,67 @@
+<script>
+const PopupDialog = {
+ name: 'popup-dialog',
+
+ props: {
+ open: Boolean,
+ title: String,
+ body: String,
+ kind: {
+ type: String,
+ default: 'primary',
+ },
+ closeButtonLabel: {
+ type: String,
+ default: 'Cancel',
+ },
+ primaryButtonLabel: {
+ type: String,
+ default: 'Save changes',
+ },
+ },
+
+ computed: {
+ typeOfClass() {
+ const className = `btn-${this.kind}`;
+ const returnObj = {};
+ returnObj[className] = true;
+ return returnObj;
+ },
+ },
+
+ methods: {
+ close() {
+ this.$emit('toggle', false);
+ },
+
+ yesClick() {
+ this.$emit('submit', true);
+ },
+
+ noClick() {
+ this.$emit('submit', false);
+ },
+ },
+};
+
+export default PopupDialog;
+</script>
+<template>
+<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">{{this.title}}</h4>
+ </div>
+ <div class="modal-body">
+ <p>{{this.body}}</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
+ <button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index 00676bcb0b3..51ed2b4fd15 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,6 +1,5 @@
/* global Breakpoints */
-import 'vendor/jquery.nicescroll';
import './breakpoints';
export default class Wikis {
@@ -8,7 +7,6 @@ export default class Wikis {
this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarExpanded = false;
- $(this.sidebarEl).niceScroll();
const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
for (let i = 0; i < sidebarToggles.length; i += 1) {
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 6ce331a9129..b2b3297e880 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -26,6 +26,7 @@
@import "framework/lists";
@import "framework/logo";
@import "framework/markdown_area";
+@import "framework/media_object";
@import "framework/mobile";
@import "framework/modal";
@import "framework/nav";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index cb41df8a88d..486d88efbc5 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -100,6 +100,8 @@
margin: 0;
align-self: center;
}
+
+ &.s40 { min-width: 40px; min-height: 40px; }
}
.avatar-counter {
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 0ac095f7d8f..0ded4a3b423 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -45,6 +45,7 @@
margin-top: -23px;
float: right;
font-size: 12px;
+ direction: ltr;
}
.pika-single.gitlab-theme {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index bd4bd541c3a..02e0ba74158 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -728,6 +728,10 @@
@mixin new-style-dropdown {
.dropdown-menu,
.dropdown-menu-nav {
+ .divider {
+ margin: 6px 0;
+ }
+
li {
padding: 0 1px;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 555e444a062..d9f92e93160 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -26,7 +26,7 @@ header {
&.navbar-gitlab {
padding: 0 16px;
- z-index: 400;
+ z-index: 2000;
margin-bottom: 0;
min-height: $header-height;
background-color: $gray-light;
@@ -325,9 +325,9 @@ header {
li {
.badge {
position: inherit;
- top: -3px;
+ top: -8px;
font-weight: normal;
- margin-left: -12px;
+ margin-left: -11px;
font-size: 11px;
color: $white-light;
padding: 1px 5px 2px;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 67c3287ed74..bd0367f86dd 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -109,18 +109,20 @@ body {
}
}
-
-/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
-which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
-effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
-of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */
-
-.navbar,
-.page-gutter,
-.page-with-sidebar {
- -webkit-overflow-scrolling: auto;
+.page-with-sidebar > .content-wrapper {
+ min-height: calc(100vh - #{$header-height});
}
.with-performance-bar .page-with-sidebar {
margin-top: $header-height + $performance-bar-height;
}
+
+[v-cloak] {
+ display: none;
+}
+
+.vertical-center {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+}
diff --git a/app/assets/stylesheets/framework/media_object.scss b/app/assets/stylesheets/framework/media_object.scss
new file mode 100644
index 00000000000..b573052c14a
--- /dev/null
+++ b/app/assets/stylesheets/framework/media_object.scss
@@ -0,0 +1,8 @@
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+
+.media-body {
+ flex: 1;
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 88e7ba117d5..d386ac5ba9c 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -251,7 +251,6 @@
// Applies on /dashboard/issues
.project-item-select-holder {
- display: block;
margin: 0;
}
}
@@ -283,6 +282,31 @@
}
}
+.project-item-select-holder.btn-group {
+ display: flex;
+ max-width: 350px;
+ overflow: hidden;
+
+ @media(max-width: $screen-xs-max) {
+ width: 100%;
+ max-width: none;
+ }
+
+ .new-project-item-link {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .new-project-item-select-button {
+ width: 32px;
+ }
+}
+
+.new-project-item-select-button .fa-caret-down {
+ margin-left: 2px;
+}
+
.layout-nav {
width: 100%;
background: $gray-light;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 09b60ad1676..40e8a928e6e 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -78,15 +78,12 @@
.right-sidebar {
border-left: 1px solid $border-color;
+ height: calc(100% - #{$header-height});
&.affix {
position: fixed;
top: $header-height;
}
-
- &:not(.affix-top) {
- min-height: 100%;
- }
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 0df6f24bfe6..3c109a5a929 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -88,6 +88,7 @@ $indigo-950: #1a1a40;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
+$almost-black: #242424;
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
@@ -206,7 +207,6 @@ $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
-
/*
* Common component specific colors
*/
@@ -316,6 +316,12 @@ $badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary;
/*
+ * Status icons
+ */
+$status-icon-size: 22px;
+$status-icon-margin: $gl-btn-padding;
+
+/*
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0, 0, 0, .175);
@@ -614,6 +620,13 @@ $color-average-score: $orange-400;
$color-low-score: $red-400;
/*
+Repo editor
+*/
+$repo-editor-grey: #f6f7f9;
+$repo-editor-grey-darker: #e9ebee;
+$repo-editor-linear-gradient: linear-gradient(to right, $repo-editor-grey 0%, $repo-editor-grey-darker, 20%, $repo-editor-grey 40%, $repo-editor-grey 100%);
+
+/*
Performance Bar
*/
$perf-bar-text: #999;
@@ -624,3 +637,11 @@ $perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
+
+
+/*
+Project Templates Icons
+*/
+$rails: #c00;
+$node: #353535;
+$java: #70ad51;
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 1c4a84de7ec..795ee91af8b 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -312,6 +312,10 @@ header.navbar-gitlab-new {
// TODO: fallback to global style
.dropdown-menu {
+ .divider {
+ margin: 6px 0;
+ }
+
li {
padding: 0 1px;
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 3d202183c82..76dccd2df56 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -8,20 +8,25 @@ $active-color: $indigo-700;
$active-hover-background: $active-background;
$active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08);
-$hover-background: $indigo-700;
-$hover-color: $white-light;
+$hover-background: $white-light;
+$hover-color: $gl-text-color;
$inactive-color: $gl-text-color-secondary;
$new-sidebar-width: 220px;
+$new-sidebar-collapsed-width: 50px;
.page-with-new-sidebar {
- @media (min-width: $screen-sm-min) {
+ @media (min-width: $screen-md-min) {
+ padding-left: $new-sidebar-collapsed-width;
+ }
+
+ @media (min-width: $screen-lg-min) {
padding-left: $new-sidebar-width;
}
// Override position: absolute
.right-sidebar {
position: fixed;
- height: 100%;
+ height: calc(100% - #{$header-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
@@ -29,8 +34,15 @@ $new-sidebar-width: 220px;
}
}
+.page-with-icon-sidebar {
+ @media (min-width: $screen-sm-min) {
+ padding-left: $new-sidebar-collapsed-width;
+ }
+}
+
.context-header {
position: relative;
+ margin-right: 2px;
a {
border-bottom: 1px solid $border-color;
@@ -39,26 +51,16 @@ $new-sidebar-width: 220px;
align-items: center;
padding: 10px 16px 10px 10px;
color: $gl-text-color;
+ }
- @media (max-width: $screen-xs-max) {
- padding-right: 30px;
- }
-
- &:hover {
- background-color: $hover-background;
- color: $hover-color;
- border-color: $hover-background;
-
- .avatar-container {
- border-color: transparent;
- }
-
- .settings-avatar {
- background-color: $indigo-500;
+ &:hover,
+ a:hover {
+ background-color: $hover-background;
+ color: $hover-color;
- i {
- color: $hover-color;
- }
+ .settings-avatar {
+ i {
+ color: $hover-color;
}
}
}
@@ -73,32 +75,6 @@ $new-sidebar-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
-
-
- &:hover {
- .close-nav-button {
- color: $white-light;
- }
- }
-
- .close-nav-button {
- display: none;
- position: absolute;
- top: 0;
- right: 0;
- height: 100%;
- background-color: transparent;
- border: 0;
- padding: 0 10px;
-
- @media (max-width: $screen-xs-max) {
- display: block;
- }
-
- &:hover {
- color: $gl-text-color;
- }
- }
}
.settings-avatar {
@@ -125,6 +101,16 @@ $new-sidebar-width: 220px;
background-color: $gray-normal;
box-shadow: inset -2px 0 0 $border-color;
+ &.sidebar-icons-only {
+ width: $new-sidebar-collapsed-width;
+
+ .nav-item-name,
+ .badge,
+ .project-title {
+ display: none;
+ }
+ }
+
&.nav-sidebar-expanded {
left: 0;
}
@@ -219,6 +205,8 @@ $new-sidebar-width: 220px;
}
.sidebar-top-level-items {
+ margin-bottom: 60px;
+
> li {
> a {
@media (min-width: $screen-sm-min) {
@@ -233,14 +221,14 @@ $new-sidebar-width: 220px;
&:not(.active) {
> a {
margin-left: 1px;
- margin-right: 3px;
+ margin-right: 2px;
}
.sidebar-sub-level-items {
@media (min-width: $screen-sm-min) {
position: fixed;
top: 0;
- left: 220px;
+ left: $new-sidebar-width;
width: 150px;
margin-top: -1px;
padding: 8px 1px;
@@ -326,6 +314,95 @@ $new-sidebar-width: 220px;
}
}
+
+// Collapsed nav
+
+.toggle-sidebar-button,
+.close-nav-button {
+ width: $new-sidebar-width - 2px;
+ position: fixed;
+ bottom: 0;
+ padding: 16px;
+ background-color: $gray-normal;
+ border: 0;
+ border-top: 2px solid $border-color;
+ color: $gl-text-color-secondary;
+ display: flex;
+ align-items: center;
+
+ i {
+ font-size: 20px;
+ margin-right: 8px;
+ }
+
+ .fa-angle-double-right {
+ display: none;
+ }
+
+ &:hover {
+ background-color: $border-color;
+ color: $gl-text-color;
+ }
+}
+
+.toggle-sidebar-button {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+}
+
+
+.sidebar-icons-only {
+ .context-header {
+ height: 60px;
+
+ a {
+ padding: 10px 4px;
+ }
+ }
+
+ li a {
+ padding: 12px 15px;
+ }
+
+ .sidebar-top-level-items > li {
+ &.active a {
+ padding-left: 12px;
+ }
+
+ .sidebar-sub-level-items {
+ @media (min-width: $screen-sm-min) {
+ left: $new-sidebar-collapsed-width;
+ }
+
+ &:not(.flyout-list) {
+ display: none;
+ }
+ }
+ }
+
+ .toggle-sidebar-button {
+ width: $new-sidebar-collapsed-width - 2px;
+ padding: 16px 18px;
+
+ .collapse-text,
+ .fa-angle-double-left {
+ display: none;
+ }
+
+ .fa-angle-double-right {
+ display: block;
+ }
+ }
+}
+
+
+// Mobile nav
+
+.close-nav-button {
+ display: none;
+}
+
.toggle-mobile-nav {
display: none;
background-color: transparent;
@@ -345,6 +422,12 @@ $new-sidebar-width: 220px;
}
}
+@media (max-width: $screen-xs-max) {
+ .close-nav-button {
+ display: flex;
+ }
+}
+
.mobile-overlay {
display: none;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 6039cda96d8..e5b467a2691 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -165,6 +165,7 @@
.board-title {
padding-top: ($gl-padding - 3px);
+ padding-bottom: $gl-padding;
}
}
}
@@ -178,6 +179,7 @@
position: relative;
margin: 0;
padding: $gl-padding;
+ padding-bottom: ($gl-padding + 3px);
font-size: 1em;
border-bottom: 1px solid $border-color;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 28c99d8e57c..486424fb729 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -235,8 +235,18 @@
display: none;
}
+ .sidebar-container {
+ width: calc(100% + 100px);
+ padding-right: 100px;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
.blocks-container {
padding: 0 $gl-padding;
+ width: 289px;
}
.block {
@@ -259,7 +269,15 @@
padding: 16px 0;
}
+ .trigger-build-variables {
+ margin: 0;
+ overflow-x: auto;
+ -ms-overflow-style: scrollbar;
+ -webkit-overflow-scrolling: touch;
+ }
+
.trigger-build-variable {
+ font-weight: normal;
color: $code-color;
}
@@ -326,6 +344,7 @@
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
max-height: 300px;
+ width: 289px;
overflow: auto;
svg {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6da14320914..b78db402c13 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -5,6 +5,30 @@
margin-right: auto;
}
+.is-confidential {
+ color: $orange-600;
+ background-color: $orange-50;
+ border-radius: 3px;
+ padding: 5px;
+ margin: 0 3px 0 -4px;
+}
+
+.is-not-confidential {
+ border-radius: 3px;
+ padding: 5px;
+ margin: 0 3px 0 -4px;
+}
+
+.confidentiality {
+ .is-not-confidential {
+ margin: auto;
+ }
+
+ .is-confidential {
+ margin: auto;
+ }
+}
+
.limit-container-width {
.detail-page-header,
.page-content-header,
@@ -328,9 +352,17 @@
margin-bottom: 10px;
color: $issuable-sidebar-color;
+ svg {
+ fill: $issuable-sidebar-color;
+ }
+
&:hover,
&:hover .todo-undone {
color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
}
span {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index a4e19094508..6bb013cca85 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -2,10 +2,35 @@
* MR -> show: Automerge widget
*
*/
+
+.space-children {
+ @include clearfix;
+
+ > * {
+ float: left;
+ }
+
+ > *:not(:last-child) {
+ margin-right: 10px;
+ }
+}
+
.mr-state-widget {
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 2px;
+ line-height: 28px;
+
+ .mr-widget-heading,
+ .mr-widget-section,
+ .mr-widget-footer {
+ padding: $gl-padding;
+ border-top: solid 1px $border-color;
+ }
+
+ .mr-widget-footer {
+ padding: 0;
+ }
form {
margin-bottom: 0;
@@ -15,15 +40,35 @@
}
}
+ label {
+ margin-bottom: 0;
+ }
+
+ .btn {
+ font-size: $gl-font-size;
+
+ &[disabled] {
+ opacity: 0.3;
+ }
+
+ &.btn-xs {
+ line-height: 1;
+ padding: 5px 10px;
+ margin-top: 1px;
+ }
+
+ &.dropdown-toggle {
+ .fa {
+ color: inherit;
+ }
+ }
+ }
+
.accept-merge-holder {
.accept-action {
display: inline-block;
float: left;
- .btn-success.dropdown-toggle .fa {
- color: inherit;
- }
-
.accept-merge-request {
&.ci-pending,
&.ci-running {
@@ -84,77 +129,64 @@
.ci-widget {
color: $gl-text-color;
- display: -webkit-flex;
display: flex;
- -webkit-align-items: center;
- align-items: center;
- padding: $gl-padding-top $gl-padding 0;
-
- svg {
- position: relative;
- top: 1px;
- overflow: visible;
- }
-
- > span {
- padding-right: 4px;
- }
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
+ }
- .icon-link > .ci-status-icon > svg {
- width: 22px;
- height: 22px;
- margin-right: 8px;
- }
+ .mr-widget-icon {
+ font-size: 22px;
+ margin-right: $status-icon-margin;
+ }
- .ci-error {
- margin-right: $btn-side-margin;
- }
+ .ci-status-icon svg {
+ width: $status-icon-size;
+ height: $status-icon-size;
+ margin: 3px 0;
+ position: relative;
+ overflow: visible;
+ display: block;
}
- .mr-widget-body,
- .mr-widget-footer {
- margin: 16px;
+ .mr-widget-body {
+ @include clearfix;
+
+ &.media > *:first-child {
+ margin-right: 10px;
+ }
}
.mr-widget-pipeline-graph {
- flex-shrink: 0;
+ padding: 0 4px;
.dropdown-menu {
- margin-top: 11px;
z-index: 300;
}
.ci-action-icon-wrapper {
line-height: 16px;
}
+ }
- @media (max-width: $screen-xs-max) {
- order: 1;
- margin-top: $gl-padding-top;
- border-radius: 3px;
- background-color: $white-light;
- border: 1px solid $gray-darker;
- width: 100%;
- text-align: center;
+ .mini-pipeline-graph-dropdown-toggle {
+ vertical-align: top;
+ }
- .dropdown-menu {
- margin-left: -97.5px;
- }
+ .mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
+ display: flex;
+ align-items: center;
- .arrow-up::before,
- .arrow-up::after, {
- margin-left: 97.5px;
- }
+ .ci-status-text,
+ .ci-status-icon {
+ top: 0;
+ margin-right: 10px;
}
}
.normal {
- color: $gl-text-color;
- font-size: 15px;
+ line-height: 28px;
}
.capitalize {
@@ -165,9 +197,8 @@
@extend .ref-name;
color: $gl-text-color;
- font-weight: bold;
+ font-weight: 600;
overflow: hidden;
- margin: 0 3px;
word-break: break-all;
&.label-truncated {
@@ -189,52 +220,19 @@
}
}
- .js-deployment-link {
- display: inline-block;
- }
-
.mr-widget-help {
- margin: $gl-padding;
- color: $ci-skipped-color;
- }
-
- .mr-info-list {
-
- &.mr-links {
- margin-left: 28px;
- }
-
- &.mr-memory-usage {
- margin: 5px 0 10px 25px;
- }
- }
-
- .mr-widget-heading {
- .btn-default.btn-xs {
- margin-left: 5px;
- }
- }
-
- .mr-widget-body {
- .btn {
- font-size: 15px;
- }
-
- .btn-group .btn {
- padding: 5px 10px;
-
- &.dropdown-toggle {
- padding: 5px 7px;
- }
- }
+ padding: 10px 16px 10px 48px;
+ font-style: italic;
}
.mr-widget-body {
h4 {
- font-weight: bold;
- font-size: 15px;
- margin: 5px 0;
- color: $gl-text-color;
+ float: left;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: inherit;
+ margin-top: 0;
+ margin-bottom: 0;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
@@ -255,18 +253,16 @@
}
.spacing {
- margin: 0 $gl-padding;
+ margin: 0 0 0 10px;
}
.bold {
- font-weight: bold;
- font-size: 15px;
+ font-weight: 600;
color: $gl-gray-light;
}
.state-label {
- font-size: 16px;
- font-weight: bold;
+ font-weight: 600;
padding-right: 10px;
}
@@ -274,16 +270,6 @@
color: $gl-danger;
}
- .mr-widget-help {
- margin: $gl-padding 0;
- }
-
- .with-button {
- position: relative;
- top: 6px;
- margin-bottom: 24px;
- }
-
.spacing,
.bold {
vertical-align: middle;
@@ -294,15 +280,8 @@
padding: 5px;
}
- .merge-opt-icon,
- .merge-opt-title {
- display: inline-block;
- float: left;
- }
-
- .merge-opt-icon svg {
- height: 15px;
- width: 15px;
+ .merge-opt-icon {
+ line-height: 1.5;
}
.merge-opt-title {
@@ -316,34 +295,15 @@
}
}
- .has-error-message + .has-custom-error {
- margin-left: 0;
- }
-
.has-custom-error {
display: inline-block;
- margin-left: 70px;
- }
-
- .merge-error-text {
- margin-left: 70px;
}
@media (max-width: $screen-xs-max) {
- h4 {
- font-size: 14px;
- }
-
p {
font-size: 13px;
}
- .btn,
- .btn-group,
- .accept-action {
- margin-bottom: 4px;
- }
-
.btn-grouped {
float: none;
margin-right: 0;
@@ -367,19 +327,16 @@
}
}
- &.mr-state-locked .mr-info-list {
- margin-top: 10px;
- margin-left: 12px;
- }
+ &.mr-widget-empty-state {
+ line-height: 20px;
- &.empty-state {
.artwork {
margin-bottom: $gl-padding;
}
.text {
span {
- font-weight: bold;
+ font-weight: 600;
}
p {
@@ -389,10 +346,6 @@
}
}
- .mr-widget-footer {
- border-top: 1px solid $gray-darker;
- }
-
.ci-coverage {
float: right;
}
@@ -497,8 +450,6 @@
}
.btn-clipboard {
- @extend .pull-right;
-
margin-right: 20px;
margin-top: 5px;
position: absolute;
@@ -506,56 +457,29 @@
}
}
+.mr-links {
+ padding-left: $status-icon-size + $status-icon-margin;
+}
+
.mr-info-list {
+ clear: left;
position: relative;
- margin: 10px 0 $gl-padding 12px;
+ padding-top: 4px;
p {
- margin: 6px 0;
+ margin: 0;
position: relative;
- padding-left: 15px;
-
- &::before {
- content: '';
- position: absolute;
- border-top: 2px solid $border-color;
- height: 1px;
- top: 9px;
- width: 8px;
- left: 0;
- }
+ padding: 4px 0;
&:last-child {
- margin-bottom: 0;
+ padding-bottom: 0;
}
}
-
- .legend {
- height: 100%;
- width: 2px;
- background: $border-color;
- position: absolute;
- top: -9px;
- }
}
.mr-info-list.mr-memory-usage {
- .legend {
- height: 65%;
- top: 0;
-
- @media (max-width: $screen-xs-max) {
- height: 20px;
- }
- }
-
p {
float: left;
- padding-left: 21px;
-
- &::before {
- top: 13px;
- }
}
.memory-graph-container {
@@ -565,12 +489,13 @@
}
.mr-source-target {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
background-color: $gray-light;
- border-radius: 3px 3px 0 0;
- border-bottom: 1px solid $border-color;
- padding: 0 $gl-padding;
- margin-bottom: 6px;
- line-height: 44px;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ padding: $gl-padding / 2 $gl-padding;
.dropdown-toggle .fa {
color: $gl-text-color;
@@ -679,14 +604,8 @@
}
.merged-buttons {
- margin-top: 20px;
-
.btn {
float: left;
-
- &:not(:last-child) {
- margin-right: 10px;
- }
}
}
@@ -803,20 +722,8 @@
}
.mr-memory-usage {
- p.usage-info-loading,
- p.usage-info-unavailable,
- p.usage-info-failed {
- margin-bottom: 5px;
- }
-
p.usage-info-loading .usage-info-load-spinner {
margin-right: 10px;
font-size: 16px;
}
-
- @media (max-width: $screen-md-min) {
- .mr-info-list.mr-memory-usage .legend {
- height: 80%;
- }
- }
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index cdb1e65e4be..c90642178fc 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -104,40 +104,51 @@
}
.confidential-issue-warning {
- background-color: $gray-normal;
- border-radius: 3px;
+ color: $orange-600;
+ background-color: $orange-50;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ border: 1px solid $border-gray-normal;
padding: 3px 12px;
margin: auto;
- margin-top: 0;
- text-align: center;
- font-size: 12px;
align-items: center;
+}
- @media (max-width: $screen-md-max) {
- // On smaller devices the warning becomes the fourth item in the list,
- // rather than centering, and grows to span the full width of the
- // comment area.
- order: 4;
- margin: 6px auto;
- width: 100%;
+.confidential-value {
+ .fa {
+ background-color: inherit;
}
+}
- .fa {
- margin-right: 8px;
+.confidential-warning-message {
+ line-height: 1.5;
+ padding: 16px;
+
+ .confidential-warning-message-actions {
+ display: flex;
+
+ button {
+ flex-grow: 1;
+ }
}
}
+.not-confidential {
+ padding: 0;
+ border-top: none;
+}
+
.right-sidebar-expanded {
- .confidential-issue-warning {
- // When the sidebar is open the warning becomes the fourth item in the list,
- // rather than centering, and grows to span the full width of the
- // comment area.
- order: 4;
- margin: 6px auto;
- width: 100%;
+ .md-area {
+ border-radius: 0;
+ border-top: none;
}
}
+.right-sidebar-collapsed {
+ .confidential-issue-warning {
+ border-bottom: none;
+ }
+}
.discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index d3862df20d3..6185342b495 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -220,7 +220,11 @@
position: relative;
vertical-align: middle;
height: 22px;
- margin: 3px 6px 3px 0;
+ margin: 3px 0;
+
+ + .stage-container {
+ margin-left: 6px;
+ }
// Hack to show a button tooltip inline
button.has-tooltip + .tooltip {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index d29421aa1b3..276465488e7 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -7,7 +7,8 @@
}
.new_project,
-.edit-project {
+.edit-project,
+.import-project {
.sharing-and-permissions {
.header {
@@ -36,7 +37,6 @@
}
select {
- background: transparent;
transition: background 2s ease-out;
&.highlight-changes {
@@ -458,6 +458,7 @@ a.deploy-project-label {
}
}
+.project-template,
.project-import {
.form-group {
margin-bottom: 5px;
@@ -472,7 +473,44 @@ a.deploy-project-label {
.btn {
padding: 8px;
- margin-left: 10px;
+ margin-right: 10px;
+ }
+
+ .blank-option {
+ min-width: 70px;
+ }
+
+ .btn-template-icon {
+ height: 24px;
+ width: inherit;
+ display: block;
+ margin: 0 auto 4px;
+ font-size: 24px;
+
+ @media (min-width: $screen-xs-max) {
+ top: 0;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .btn-template-icon {
+ display: inline-block;
+ height: 14px;
+ font-size: 14px;
+ margin: 0;
+ }
+ }
+
+ .icon-rails path {
+ fill: $rails;
+ }
+
+ .icon-node-express path {
+ fill: $node;
+ }
+
+ .icon-java-spring path {
+ fill: $java;
}
> div {
@@ -482,6 +520,97 @@ a.deploy-project-label {
}
}
+.project-templates-buttons .btn:last-child {
+ margin-right: 0;
+}
+
+.create-project-options {
+ display: flex;
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+ }
+
+ .first-column {
+ @media(min-width: $screen-xs-min) {
+ max-width: 50%;
+ padding-right: 30px;
+ }
+
+ @media(max-width: $screen-xs-max) {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+
+ .second-column {
+ @media(min-width: $screen-xs-min) {
+ width: 50%;
+ flex: 1;
+ padding-left: 30px;
+ position: relative;
+ }
+
+ @media(max-width: $screen-xs-max) {
+ max-width: 100%;
+ width: 100%;
+ padding-left: 0;
+ position: relative;
+ }
+
+ // Mobile
+ @media (max-width: $screen-xs-max) {
+ padding-top: 30px;
+ }
+
+ &::before {
+ content: "OR";
+ position: absolute;
+ left: 0;
+ top: 40%;
+ z-index: 10;
+ padding: 8px 0;
+ text-align: center;
+ background-color: $white-light;
+ color: $gl-text-color-tertiary;
+ transform: translateX(-50%);
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 20px;
+
+ // Mobile
+ @media (max-width: $screen-xs-max) {
+ left: 50%;
+ top: 10px;
+ transform: translateY(-50%);
+ padding: 0 8px;
+ }
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ background-color: $border-color;
+ bottom: 0;
+ left: 0;
+ right: auto;
+ height: 100%;
+ width: 1px;
+ top: 0;
+
+ // Mobile
+ @media (max-width: $screen-xs-max) {
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ height: 1px;
+ width: auto;
+ }
+ }
+ }
+}
+
+
.project-stats {
font-size: 0;
text-align: center;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
new file mode 100644
index 00000000000..ad17078c98a
--- /dev/null
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -0,0 +1,413 @@
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity .5s;
+}
+
+.monaco-loader {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: $black-transparent;
+}
+
+.modal.popup-dialog {
+ display: block;
+ background-color: $black-transparent;
+ z-index: 2100;
+
+ @media (min-width: $screen-md-min) {
+ .modal-dialog {
+ width: 600px;
+ margin: 30px auto;
+ }
+ }
+}
+
+.project-refs-form,
+.project-refs-target-form {
+ display: inline-block;
+
+ &.disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.commit-message {
+ @include str-truncated(250px);
+}
+
+.editable-mode {
+ display: inline-block;
+}
+
+.blob-viewer[data-type="rich"] {
+ margin: 20px;
+}
+
+.repository-view.tree-content-holder {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ color: $almost-black;
+
+ .panel-right {
+ display: inline-block;
+ width: 80%;
+
+ .monaco-editor.vs {
+ .line-numbers {
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .cursor {
+ display: none !important;
+ }
+ }
+
+ &.edit-mode {
+ .blob-viewer-container {
+ overflow: hidden;
+ }
+
+ .monaco-editor.vs {
+ .cursor {
+ background: $black;
+ border-color: $black;
+ display: block !important;
+ }
+ }
+ }
+
+ .blob-viewer-container {
+ height: calc(100vh - 63px);
+ overflow: auto;
+ }
+
+ #tabs {
+ padding-left: 0;
+ margin-bottom: 0;
+ display: flex;
+ white-space: nowrap;
+ width: 100%;
+ overflow-y: hidden;
+ overflow-x: auto;
+
+ li {
+ animation: swipeRightAppear ease-in 0.1s;
+ animation-iteration-count: 1;
+ transform-origin: 0% 50%;
+ list-style-type: none;
+ background: $gray-normal;
+ display: inline-block;
+ padding: 10px 18px;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ white-space: nowrap;
+
+ &.remove {
+ animation: swipeRightDissapear ease-in 0.1s;
+ animation-iteration-count: 1;
+ transform-origin: 0% 50%;
+
+ a {
+ width: 0;
+ }
+ }
+
+ &.active {
+ background: $white-light;
+ border-bottom: none;
+ }
+
+ a {
+ @include str-truncated(100px);
+ color: $black;
+ display: inline-block;
+ width: 100px;
+ text-align: center;
+ vertical-align: middle;
+
+ &.close {
+ width: auto;
+ font-size: 15px;
+ opacity: 1;
+ margin-right: -6px;
+ }
+ }
+
+ i.fa.fa-times,
+ i.fa.fa-circle {
+ float: right;
+ margin-top: 3px;
+ margin-left: 15px;
+ color: $gray-darkest;
+ }
+
+ i.fa.fa-circle {
+ color: $brand-success;
+ }
+
+ &.tabs-divider {
+ width: 100%;
+ background-color: $white-light;
+ border-right: none;
+ border-top-right-radius: 2px;
+ }
+ }
+ }
+
+ #repo-file-buttons {
+ background-color: $white-light;
+ border-bottom: 1px solid $white-normal;
+ padding: 5px 10px;
+ position: relative;
+ border-top: 1px solid $white-normal;
+ margin-top: -5px;
+ }
+
+ #binary-viewer {
+ height: 80vh;
+ overflow: auto;
+ margin: 0;
+
+ .blob-viewer {
+ padding-top: 20px;
+ padding-left: 20px;
+ }
+
+ .binary-unknown {
+ text-align: center;
+ padding-top: 100px;
+ background: $gray-light;
+ height: 100%;
+ font-size: 17px;
+
+ span {
+ display: block;
+ }
+ }
+ }
+ }
+
+ #commit-area {
+ background: $gray-light;
+ padding: 20px;
+
+ span.help-block {
+ padding-top: 7px;
+ margin-top: 0;
+ }
+ }
+
+ #view-toggler {
+ height: 41px;
+ position: relative;
+ display: block;
+ border-bottom: 1px solid $white-normal;
+ background: $white-light;
+ margin-top: -5px;
+ }
+
+ #binary-viewer {
+ img {
+ max-width: 100%;
+ }
+ }
+
+ #sidebar {
+
+ &.sidebar-mini {
+ display: inline-block;
+ vertical-align: top;
+ width: 20%;
+ border-right: 1px solid $white-normal;
+ height: calc(100vh + 20px);
+ overflow: auto;
+ }
+
+ table {
+ margin-bottom: 0;
+ }
+
+ tr {
+ animation: fadein 0.5s;
+ cursor: pointer;
+
+ &.repo-file-options td {
+ padding: 0;
+ border-top: none;
+ background: $gray-light;
+ width: 100%;
+ display: inline-block;
+
+ &:first-child {
+ border-top-left-radius: 2px;
+ }
+
+ .title {
+ display: inline-block;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: bold;
+ color: $gray-darkest;
+ width: 185px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ padding: 2px 16px;
+ }
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
+
+ td {
+ white-space: nowrap;
+ }
+ }
+
+ a {
+ color: $almost-black;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ ul {
+ list-style-type: none;
+ padding: 0;
+
+ li {
+ border-bottom: 1px solid $border-gray-normal;
+ padding: 10px 20px;
+
+ a {
+ color: $almost-black;
+ }
+
+ .fa {
+ font-size: $code_font_size;
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+
+}
+
+.animation-container {
+ background: $repo-editor-grey;
+ height: 40px;
+ overflow: hidden;
+ position: relative;
+
+ &.animation-container-small {
+ height: 12px;
+ }
+
+ &::before {
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: blockTextShine;
+ animation-timing-function: linear;
+ background-image: $repo-editor-linear-gradient;
+ background-repeat: no-repeat;
+ background-size: 800px 45px;
+ content: ' ';
+ display: block;
+ height: 100%;
+ position: relative;
+ }
+
+ div {
+ background: $white-light;
+ height: 6px;
+ left: 0;
+ position: absolute;
+ right: 0;
+ }
+
+ .line-of-code-1 {
+ left: 0;
+ top: 8px;
+ }
+
+ .line-of-code-2 {
+ left: 150px;
+ top: 0;
+ height: 10px;
+ }
+
+ .line-of-code-3 {
+ left: 0;
+ top: 23px;
+ }
+
+ .line-of-code-4 {
+ left: 0;
+ top: 38px;
+ }
+
+ .line-of-code-5 {
+ left: 200px;
+ top: 28px;
+ height: 10px;
+ }
+
+ .line-of-code-6 {
+ top: 14px;
+ left: 230px;
+ height: 10px;
+ }
+}
+
+.render-error {
+ min-height: calc(100vh - 63px);
+
+ p {
+ width: 100%;
+ }
+}
+
+@keyframes blockTextShine {
+ 0% {
+ transform: translateX(-468px);
+ }
+
+ 100% {
+ transform: translateX(468px);
+ }
+}
+
+@keyframes swipeRightAppear {
+ 0% {
+ transform: scaleX(0.00);
+ }
+
+ 100% {
+ transform: scaleX(1.00);
+ }
+}
+
+@keyframes swipeRightDissapear {
+ 0% {
+ transform: scaleX(1.00);
+ }
+
+ 100% {
+ transform: scaleX(0.00);
+ }
+}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index d69a8e0995c..15df51e9c69 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -54,8 +54,7 @@
.settings-content {
max-height: 1px;
overflow-y: scroll;
- margin-right: -20px;
- padding-right: 130px;
+ padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
&.expanded {
@@ -87,6 +86,23 @@
overflow: hidden;
margin-top: 20px;
}
+
+ .sub-section {
+ margin-bottom: 32px;
+ padding: 16px;
+ border: 1px solid $border-color;
+ background-color: $gray-light;
+ }
+
+ .bs-callout,
+ .checkbox:first-child,
+ .help-block {
+ margin-top: 0;
+ }
+
+ .label-light {
+ margin-bottom: 0;
+ }
}
.settings-list-icon {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 44ab07a4367..a8e0f251cd3 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -87,7 +87,7 @@
}
.add-to-tree {
- vertical-align: top;
+ vertical-align: middle;
padding: 6px 10px;
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 45c21c5d274..fa6bdd297eb 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -95,12 +95,22 @@
}
.right-sidebar.wiki-sidebar {
- padding: $gl-padding 0;
+ padding: 0;
&.right-sidebar-collapsed {
display: none;
}
+ .sidebar-container {
+ padding: $gl-padding 0;
+ width: calc(100% + 100px);
+ padding-right: 100px;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
.blocks-container {
padding: 0 $gl-padding;
}