diff options
Diffstat (limited to 'app/assets')
104 files changed, 3778 insertions, 957 deletions
diff --git a/app/assets/images/new_repo.png b/app/assets/images/new_repo.png Binary files differnew file mode 100644 index 00000000000..ed3af06ab1d --- /dev/null +++ b/app/assets/images/new_repo.png diff --git a/app/assets/images/old_repo.png b/app/assets/images/old_repo.png Binary files differnew file mode 100644 index 00000000000..c3c3b791ad9 --- /dev/null +++ b/app/assets/images/old_repo.png 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">×</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; } |