diff options
Diffstat (limited to 'app')
314 files changed, 4317 insertions, 2648 deletions
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js index 38a8317dbd7..8f5e2e545ec 100644 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -10,7 +10,7 @@ class AjaxLoadingSpinner { e.target.setAttribute('disabled', ''); const iconElement = e.target.querySelector('i'); // get first fa- icon - const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first(); + const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0]; iconElement.dataset.icon = originalIcon; AjaxLoadingSpinner.toggleLoadingIcon(iconElement); $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 18cd04b176a..097f79a250a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ /* global Flash */ - +import _ from 'underscore'; import Cookies from 'js-cookie'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index b20d108aa25..035a7e5c431 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import '../commons/bootstrap'; // Requires Input behavior @@ -48,7 +49,9 @@ function hideOrShowHelpBlock(form) { $(() => { const $form = $('form.js-requires-input'); - $form.requiresInput(); - hideOrShowHelpBlock($form); - $('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form)); + if ($form) { + $form.requiresInput(); + hideOrShowHelpBlock($form); + $('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form)); + } }); diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 77e92ff8caf..b70b0a9bbf8 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,4 +1,3 @@ - // Toggle button. Show/hide content inside parent container. // Button does not change visibility. If button has icon - it changes chevron style. // diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 1c64ccf536f..b5500ac116f 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -8,6 +8,7 @@ import BlobFileDropzone from '../blob/blob_file_dropzone'; $(() => { const editBlobForm = $('.js-edit-blob-form'); const uploadBlobForm = $('.js-upload-blob-form'); + const deleteBlobForm = $('.js-delete-blob-form'); if (editBlobForm.length) { const urlRoot = editBlobForm.data('relative-url-root'); @@ -30,4 +31,8 @@ $(() => { '.btn-upload-file', ); } + + if (deleteBlobForm.length) { + new NewCommitForm(deleteBlobForm); + } }); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 88b054b76e6..89c14180149 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -2,6 +2,7 @@ /* global BoardService */ /* global Flash */ +import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; import FilteredSearchBoards from './filtered_search_boards'; diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index e7f16899362..edfe7c326db 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -1,5 +1,6 @@ /* global ListLabel */ +import _ from 'underscore'; import Cookies from 'js-cookie'; const Store = gl.issueBoards.BoardsStore; 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/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index f29b6caa1ac..72bb9e10fbc 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return */ +import _ from 'underscore'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 1e12d4ca415..43928e602d6 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ /* global List */ - +import _ from 'underscore'; import Cookies from 'js-cookie'; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 1dfa064acfd..940326dcd33 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -64,7 +64,7 @@ window.Build = (function () { $(window) .off('scroll') .on('scroll', () => { - const contentHeight = this.$buildTraceOutput.prop('scrollHeight'); + const contentHeight = this.$buildTraceOutput.height(); if (contentHeight > this.windowSize) { // means the user did not scroll, the content was updated. this.windowSize = contentHeight; @@ -105,16 +105,17 @@ window.Build = (function () { }; Build.prototype.canScroll = function () { - return document.body.scrollHeight > window.innerHeight; + return $(document).height() > $(window).height(); }; Build.prototype.toggleScroll = function () { - const currentPosition = document.body.scrollTop; - const windowHeight = window.innerHeight; + const currentPosition = $(document).scrollTop(); + const scrollHeight = $(document).height(); + const windowHeight = $(window).height(); if (this.canScroll()) { if (currentPosition > 0 && - (document.body.scrollHeight - currentPosition !== windowHeight)) { + (scrollHeight - currentPosition !== windowHeight)) { // User is in the middle of the log this.toggleDisableButton(this.$scrollTopBtn, false); @@ -124,7 +125,7 @@ window.Build = (function () { this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (document.body.scrollHeight - currentPosition === windowHeight) { + } else if (scrollHeight - currentPosition === windowHeight) { // User is at the bottom of the build log. this.toggleDisableButton(this.$scrollTopBtn, false); @@ -137,7 +138,7 @@ window.Build = (function () { }; Build.prototype.scrollDown = function () { - document.body.scrollTop = document.body.scrollHeight; + $(document).scrollTop($(document).height()); }; Build.prototype.scrollToBottom = function () { @@ -147,7 +148,7 @@ window.Build = (function () { }; Build.prototype.scrollToTop = function () { - document.body.scrollTop = 0; + $(document).scrollTop(0); this.hasBeenScrolled = true; this.toggleScroll(); }; @@ -163,7 +164,6 @@ window.Build = (function () { Build.prototype.initSidebar = function () { this.$sidebar = $('.js-build-sidebar'); - this.$sidebar.niceScroll(); }; Build.prototype.getBuildTrace = function () { @@ -178,7 +178,7 @@ window.Build = (function () { this.state = log.state; } - this.windowSize = this.$buildTraceOutput.prop('scrollHeight'); + this.windowSize = this.$buildTraceOutput.height(); if (log.append) { this.$buildTraceOutput.append(log.html); 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 510bedbf641..c11b7d5f340 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -3,6 +3,7 @@ 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'; diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 7063f59d446..6db8b3afbef 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -1,3 +1,4 @@ +import 'underscore'; import './polyfills'; import './jquery'; import './bootstrap'; 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/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 54257531284..13ba4a57293 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ - +import _ from 'underscore'; import './lib/utils/common_utils'; import { placeholderImage } from './lazy_loader'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 44791a93936..6583e471a48 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -92,7 +92,7 @@ $(() => { }); }, selectDefaultStage() { - const stage = this.state.stages.first(); + const stage = this.state.stages[0]; this.selectStage(stage); }, selectStage(stage) { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 37ddca29e71..298f737a2bc 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -94,7 +94,7 @@ const JumpToDiscussion = Vue.extend({ hasDiscussionsToJumpTo = false; } } - } else if (activeTab !== 'notes') { + } else if (activeTab !== 'show') { // If we are on the commits or builds tabs, // there are no discussions to jump to. hasDiscussionsToJumpTo = false; @@ -103,12 +103,12 @@ const JumpToDiscussion = Vue.extend({ if (!hasDiscussionsToJumpTo) { // If there are no discussions to jump to on the current page, // switch to the notes tab and jump to the first disucssion there. - window.mrTabs.activateTab('notes'); - activeTab = 'notes'; + window.mrTabs.activateTab('show'); + activeTab = 'show'; jumpToFirstDiscussion = true; } - if (activeTab === 'notes') { + if (activeTab === 'show') { discussionsSelector = '.discussion[data-discussion-id]'; discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); } @@ -156,7 +156,7 @@ const JumpToDiscussion = Vue.extend({ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); - if (activeTab === 'notes') { + if (activeTab === 'show') { $target = $target.closest('.note-discussion'); // If the next discussion is closed, toggle it open. diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d5923266c60..e95892a6189 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -8,6 +8,7 @@ /* global LabelsSelect */ /* global MilestoneSelect */ /* global Commit */ +/* global CommitsList */ /* global NewBranchForm */ /* global NotificationsForm */ /* global NotificationsDropdown */ @@ -19,15 +20,20 @@ /* global Search */ /* global Admin */ /* global NamespaceSelects */ +/* global NewCommitForm */ +/* global NewBranchForm */ /* global Project */ /* global ProjectAvatar */ /* global MergeRequest */ /* global Compare */ /* global CompareAutocomplete */ +/* global ProjectFindFile */ /* global ProjectNew */ /* global ProjectShow */ +/* global ProjectImport */ /* global Labels */ /* global Shortcuts */ +/* global ShortcutsFindFile */ /* global Sidebar */ /* global ShortcutsWiki */ @@ -73,10 +79,6 @@ import GpgBadges from './gpg_badges'; (function() { var Dispatcher; - $(function() { - return new Dispatcher(); - }); - Dispatcher = (function() { function Dispatcher() { this.initSearch(); @@ -133,6 +135,8 @@ import GpgBadges from './gpg_badges'; .init(); } + const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); + switch (page) { case 'profiles:preferences:show': initExperimentalFlags(); @@ -149,7 +153,7 @@ import GpgBadges from './gpg_badges'; break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { + if (filteredSearchEnabled) { const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } @@ -177,11 +181,17 @@ import GpgBadges from './gpg_badges'; break; case 'dashboard:issues': case 'dashboard:merge_requests': - case 'groups:issues': case 'groups:merge_requests': new ProjectSelect(); initLegacyFilters(); break; + case 'groups:issues': + if (filteredSearchEnabled) { + const filteredSearchManager = new gl.FilteredSearchManager('issues'); + filteredSearchManager.setup(); + } + new ProjectSelect(); + break; case 'dashboard:todos:index': new Todos(); break; @@ -195,7 +205,6 @@ import GpgBadges from './gpg_badges'; break; case 'explore:groups:index': new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) break; const exploreGroupsLanding = new Landing( @@ -218,6 +227,10 @@ import GpgBadges from './gpg_badges'; case 'projects:compare:show': new gl.Diff(); break; + case 'projects:branches:new': + case 'projects:branches:create': + new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); + break; case 'projects:branches:index': gl.AjaxLoadingSpinner.init(); new DeleteModal(); @@ -305,18 +318,22 @@ import GpgBadges from './gpg_badges'; container: '.js-commit-pipeline-graph', }).bindEvents(); initNotes(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:commit:pipelines': new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; - case 'projects:commits:show': + case 'projects:activity': + new gl.Activities(); shortcut_handler = new ShortcutsNavigation(); - GpgBadges.fetch(); break; - case 'projects:activity': + case 'projects:commits:show': + CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); shortcut_handler = new ShortcutsNavigation(); + GpgBadges.fetch(); break; case 'projects:show': shortcut_handler = new ShortcutsNavigation(); @@ -330,6 +347,11 @@ import GpgBadges from './gpg_badges'; break; case 'projects:edit': setupProjectEdit(); + // Initialize expandable settings panels + initSettingsPanels(); + break; + case 'projects:imports:show': + new ProjectImport(); break; case 'projects:pipelines:new': new NewBranchForm($('.js-new-pipeline-form')); @@ -387,11 +409,19 @@ import GpgBadges from './gpg_badges'; shortcut_handler = new ShortcutsNavigation(); new TreeView(); new BlobViewer(); + new NewCommitForm($('.js-create-dir-form')); $('#tree-slider').waitForImages(function() { gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:find_file:show': + const findElement = document.querySelector('.js-file-finder'); + const projectFindFile = new ProjectFindFile($(".file-finder-holder"), { + url: findElement.dataset.fileFindUrl, + treeUrl: findElement.dataset.findTreeUrl, + blobUrlTemplate: findElement.dataset.blobUrlTemplate, + }); + new ShortcutsFindFile(projectFindFile); shortcut_handler = true; break; case 'projects:blob:show': @@ -475,7 +505,7 @@ import GpgBadges from './gpg_badges'; new gl.DueDateSelectors(); break; } - switch (path.first()) { + switch (path[0]) { case 'sessions': case 'omniauth_callbacks': if (!gon.u2f) break; @@ -547,7 +577,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(); @@ -604,4 +633,8 @@ import GpgBadges from './gpg_badges'; return Dispatcher; })(); + + $(function() { + new Dispatcher(); + }); }).call(window); diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/droplab/plugins/ajax.js index c0da5866139..267b53fa4f2 100644 --- a/app/assets/javascripts/droplab/plugins/ajax.js +++ b/app/assets/javascripts/droplab/plugins/ajax.js @@ -11,6 +11,16 @@ const Ajax = { if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data); }, + preprocessing: function preprocessing(config, data) { + let results = data; + + if (config.preprocessing && !data.preprocessed) { + results = config.preprocessing(data); + AjaxCache.override(config.endpoint, results); + } + + return results; + }, init: function init(hook) { var self = this; self.destroyed = false; @@ -31,7 +41,8 @@ const Ajax = { dynamicList.outerHTML = loadingTemplate.outerHTML; } - AjaxCache.retrieve(config.endpoint) + return AjaxCache.retrieve(config.endpoint) + .then(self.preprocessing.bind(null, config)) .then((data) => self._loadData(data, config, self)) .catch(config.onError); }, diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 9ebbb22e807..6d19a6d9b3a 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,11 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */ /* global Dropzone */ - +import _ from 'underscore'; import './preview_markdown'; window.DropzoneInput = (function() { function DropzoneInput(form) { - Dropzone.autoDiscover = false; const divHover = '<div class="div-dropzone-hover"></div>'; const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; const $attachButton = form.find('.button-attach-file'); @@ -218,7 +217,7 @@ window.DropzoneInput = (function() { value = e.clipboardData.getData('text/plain'); } value = value.split("\r"); - return value.first(); + return value[0]; }; const showSpinner = function(e) { diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index cac35d6eed5..dc7672560ea 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js deleted file mode 100644 index 027222f804d..00000000000 --- a/app/assets/javascripts/extensions/array.js +++ /dev/null @@ -1,11 +0,0 @@ -// TODO: remove this - -// eslint-disable-next-line no-extend-native -Array.prototype.first = function first() { - return this[0]; -}; - -// eslint-disable-next-line no-extend-native -Array.prototype.last = function last() { - return this[this.length - 1]; -}; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 139206cc185..6d516a253bb 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + /** * Makes search request for content when user types a value in the search input. * Updates the html content of the page with the received one. diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 2615d626c4c..0bc4b6f22a9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -6,7 +6,7 @@ import './filtered_search_dropdown'; class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(options = {}) { - const { input, endpoint, symbol } = options; + const { input, endpoint, symbol, preprocessing } = options; super(options); this.symbol = symbol; this.config = { @@ -14,6 +14,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { endpoint, method: 'setData', loadingTemplate: this.loadingTemplate, + preprocessing, onError() { /* eslint-disable no-new */ new Flash('An error occured fetching the dropdown data.'); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index ef8fe071012..8d711e3213c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import FilteredSearchContainer from './container'; class DropdownUtils { @@ -50,6 +51,66 @@ class DropdownUtils { return updatedItem; } + static mergeDuplicateLabels(dataMap, newLabel) { + const updatedMap = dataMap; + const key = newLabel.title; + + const hasKeyProperty = Object.prototype.hasOwnProperty.call(updatedMap, key); + + if (!hasKeyProperty) { + updatedMap[key] = newLabel; + } else { + const existing = updatedMap[key]; + + if (!existing.multipleColors) { + existing.multipleColors = [existing.color]; + } + + existing.multipleColors.push(newLabel.color); + } + + return updatedMap; + } + + static duplicateLabelColor(labelColors) { + const colors = labelColors; + const spacing = 100 / colors.length; + + // Reduce the colors to 4 + colors.length = Math.min(colors.length, 4); + + const color = colors.map((c, i) => { + const percentFirst = Math.floor(spacing * i); + const percentSecond = Math.floor(spacing * (i + 1)); + return `${c} ${percentFirst}%, ${c} ${percentSecond}%`; + }).join(', '); + + return `linear-gradient(${color})`; + } + + static duplicateLabelPreprocessing(data) { + const results = []; + const dataMap = {}; + + data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap)); + + Object.keys(dataMap) + .forEach((key) => { + const label = dataMap[key]; + + if (label.multipleColors) { + label.color = DropdownUtils.duplicateLabelColor(label.multipleColors); + label.text_color = '#000000'; + } + + results.push(label); + }); + + results.preprocessed = true; + + return results; + } + static filterHint(config, item) { const { input, allowedKeys } = config; const updatedItem = item; @@ -62,11 +123,11 @@ class DropdownUtils { if (!allowMultiple && itemInExistingTokens) { updatedItem.droplab_hidden = true; - } else if (!lastKey || searchInput.split('').last() === ' ') { + } else if (!lastKey || _.last(searchInput.split('')) === ' ') { updatedItem.droplab_hidden = false; } else if (lastKey) { const split = lastKey.split(':'); - const tokenName = split[0].split(' ').last(); + const tokenName = _.last(split[0].split(' ')); const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; updatedItem.droplab_hidden = tokenName ? match : false; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 61cef435209..dd1c067df87 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -54,6 +54,7 @@ class FilteredSearchDropdownManager { extraArguments: { endpoint: `${this.baseEndpoint}/labels.json`, symbol: '~', + preprocessing: gl.DropdownUtils.duplicateLabelPreprocessing, }, element: this.container.querySelector('#js-dropdown-label'), }, @@ -166,7 +167,7 @@ class FilteredSearchDropdownManager { // Eg. token = 'label:' const split = lastToken.split(':'); - const dropdownName = split[0].split(' ').last(); + const dropdownName = _.last(split[0].split(' ')); this.loadDropdown(split.length > 1 ? dropdownName : ''); } else if (lastToken) { // Token has been initialized into an object because it has a value diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 7872e9e68ad..a31be2b0bc7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -20,13 +20,13 @@ class FilteredSearchManager { allowedKeys: this.filteredSearchTokenKeys.getKeys(), }); this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); - const projectPath = this.searchHistoryDropdownElement ? - this.searchHistoryDropdownElement.dataset.projectFullPath : 'project'; + const fullPath = this.searchHistoryDropdownElement ? + this.searchHistoryDropdownElement.dataset.fullPath : 'project'; let recentSearchesPagePrefix = 'issue-recent-searches'; if (this.page === 'merge_requests') { recentSearchesPagePrefix = 'merge-request-recent-searches'; } - const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`; + const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); } @@ -367,7 +367,7 @@ class FilteredSearchManager { const fragments = searchToken.split(':'); if (fragments.length > 1) { const inputValues = fragments[0].split(' '); - const tokenKey = inputValues.last(); + const tokenKey = _.last(inputValues); if (inputValues.length > 1) { inputValues.pop(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index e9278140af0..243ee4d723a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -58,29 +58,54 @@ class FilteredSearchVisualTokens { `; } + static setTokenStyle(tokenContainer, backgroundColor, textColor) { + const token = tokenContainer; + + // Labels with linear gradient should not override default background color + if (backgroundColor.indexOf('linear-gradient') === -1) { + token.style.backgroundColor = backgroundColor; + } + + token.style.color = textColor; + + if (textColor === '#FFFFFF') { + const removeToken = token.querySelector('.remove-token'); + removeToken.classList.add('inverted'); + } + + return token; + } + + static preprocessLabel(labelsEndpoint, labels) { + let processed = labels; + + if (!labels.preprocessed) { + processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels); + AjaxCache.override(labelsEndpoint, processed); + processed.preprocessed = true; + } + + return processed; + } + static updateLabelTokenColor(tokenValueContainer, tokenValue) { const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; const labelsEndpoint = `${baseEndpoint}/labels.json`; return AjaxCache.retrieve(labelsEndpoint) - .then((labels) => { - const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue); - - if (!matchingLabel) { - return; - } + .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) + .then((labels) => { + const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue); - const tokenValueStyle = tokenValueContainer.style; - tokenValueStyle.backgroundColor = matchingLabel.color; - tokenValueStyle.color = matchingLabel.text_color; + if (!matchingLabel) { + return; + } - if (matchingLabel.text_color === '#FFFFFF') { - const removeToken = tokenValueContainer.querySelector('.remove-token'); - removeToken.classList.add('inverted'); - } - }) - .catch(() => new Flash('An error occurred while fetching label colors.')); + FilteredSearchVisualTokens + .setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color); + }) + .catch(() => new Flash('An error occurred while fetching label colors.')); } static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js new file mode 100644 index 00000000000..301e82f4610 --- /dev/null +++ b/app/assets/javascripts/fly_out_nav.js @@ -0,0 +1,52 @@ +/* global bp */ +import './breakpoints'; + +export const canShowSubItems = () => bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; + +export const calculateTop = (boundingRect, outerHeight) => { + const windowHeight = window.innerHeight; + const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); + + return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height : + boundingRect.top; +}; + +export const showSubLevelItems = (el) => { + const subItems = el.querySelector('.sidebar-sub-level-items'); + + if (!subItems || !canShowSubItems()) return; + + subItems.style.display = 'block'; + el.classList.add('is-over'); + + const boundingRect = el.getBoundingClientRect(); + 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) { + subItems.classList.add('is-above'); + } +}; + +export const hideSubLevelItems = (el) => { + const subItems = el.querySelector('.sidebar-sub-level-items'); + + if (!subItems || !canShowSubItems()) return; + + el.classList.remove('is-over'); + subItems.style.display = 'none'; + subItems.classList.remove('is-above'); +}; + +export default () => { + const items = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active)')] + .filter(el => el.querySelector('.sidebar-sub-level-items')); + + items.forEach((el) => { + el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); + el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); + }); +}; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 6cb9cfe1382..5c624b79d45 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 3babe273100..7d11cd0b6b2 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,5 +1,6 @@ /* 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 */ /* global fuzzaldrinPlus */ +import _ from 'underscore'; import { isObject } from './lib/utils/type_utility'; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote; @@ -114,7 +115,7 @@ GitLabDropdownFilter = (function() { } else { elements = this.options.elements(); if (search_text) { - return elements.each(function() { + elements.each(function() { var $el, matches; $el = $(this); matches = fuzzaldrinPlus.match($el.text().trim(), search_text); @@ -127,8 +128,10 @@ GitLabDropdownFilter = (function() { } }); } else { - return elements.show().removeClass('option-hidden'); + elements.show().removeClass('option-hidden'); } + + elements.parent().find('.dropdown-menu-empty-link').toggleClass('hidden', elements.is(':visible')); } }; @@ -730,10 +733,16 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) { if (this.options.filterable) { - $(':focus').blur(); - this.dropdown.one('transitionend', () => { - this.filterInput.focus(); + const initialScrollTop = $(window).scrollTop(); + + if (this.dropdown.is('.open')) { + this.filterInput.focus(); + } + + if ($(window).scrollTop() < initialScrollTop) { + $(window).scrollTop(initialScrollTop); + } }); if (triggerFocus) { diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index a433c7ba8f0..534bc535bb6 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,6 +1,4 @@ import Chart from 'vendor/Chart'; -import ContributorsStatGraph from './stat_graph_contributors'; // export to global scope window.Chart = Chart; -window.ContributorsStatGraph = ContributorsStatGraph; diff --git a/app/assets/javascripts/graphs/graphs_charts.js b/app/assets/javascripts/graphs/graphs_charts.js new file mode 100644 index 00000000000..279ffef770f --- /dev/null +++ b/app/assets/javascripts/graphs/graphs_charts.js @@ -0,0 +1,63 @@ +import Chart from 'vendor/Chart'; + +document.addEventListener('DOMContentLoaded', () => { + const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); + + const responsiveChart = (selector, data) => { + const options = { + scaleOverlay: true, + responsive: true, + pointHitDetectionRadius: 2, + maintainAspectRatio: false, + }; + // get selector by context + const ctx = selector.get(0).getContext('2d'); + // pointing parent container to make chart.js inherit its width + const container = $(selector).parent(); + const generateChart = () => { + selector.attr('width', $(container).width()); + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + return new Chart(ctx).Bar(data, options); + }; + // enabling auto-resizing + $(window).resize(generateChart); + return generateChart(); + }; + + const chartData = (keys, values) => { + const data = { + labels: keys, + datasets: [{ + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: values, + }], + }; + return data; + }; + + const hourData = chartData(projectChartData.hour.keys, projectChartData.hour.values); + responsiveChart($('#hour-chart'), hourData); + + const dayData = chartData(projectChartData.weekDays.keys, projectChartData.weekDays.values); + responsiveChart($('#weekday-chart'), dayData); + + const monthData = chartData(projectChartData.month.keys, projectChartData.month.values); + responsiveChart($('#month-chart'), monthData); + + const data = projectChartData.languages; + const ctx = $('#languages-chart').get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + + new Chart(ctx).Pie(data, options); +}); diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/graphs/graphs_show.js new file mode 100644 index 00000000000..36bad6db3e1 --- /dev/null +++ b/app/assets/javascripts/graphs/graphs_show.js @@ -0,0 +1,21 @@ +import ContributorsStatGraph from './stat_graph_contributors'; + +document.addEventListener('DOMContentLoaded', () => { + $.ajax({ + type: 'GET', + url: document.querySelector('.js-graphs-show').dataset.projectGraphPath, + dataType: 'json', + success(data) { + const graph = new ContributorsStatGraph(); + graph.init(data); + + $('#brush_change').change(() => { + graph.change_date_header(); + graph.redraw_authors(); + }); + + $('.stat-graph').fadeIn(); + $('.loading-graph').hide(); + }, + }); +}); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index c6be4c9e8fe..cdc4fcf6573 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ +import _ from 'underscore'; import d3 from 'd3'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 0deb27e522b..f64b4638485 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ - +import _ from 'underscore'; import d3 from 'd3'; const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index c583757f3f2..77135ad1f0e 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ +import _ from 'underscore'; export default { parse_log: function(log) { diff --git a/app/assets/javascripts/groups/components/group_identicon.vue b/app/assets/javascripts/groups/components/group_identicon.vue new file mode 100644 index 00000000000..0edd820743f --- /dev/null +++ b/app/assets/javascripts/groups/components/group_identicon.vue @@ -0,0 +1,45 @@ +<script> +export default { + props: { + entityId: { + type: Number, + required: true, + }, + entityName: { + type: String, + required: true, + }, + }, + computed: { + /** + * This method is based on app/helpers/application_helper.rb#project_identicon + */ + identiconStyles() { + const allowedColors = [ + '#FFEBEE', + '#F3E5F5', + '#E8EAF6', + '#E3F2FD', + '#E0F2F1', + '#FBE9E7', + '#EEEEEE', + ]; + + const backgroundColor = allowedColors[this.entityId % 7]; + + return `background-color: ${backgroundColor}; color: #555;`; + }, + identiconTitle() { + return this.entityName.charAt(0).toUpperCase(); + }, + }, +}; +</script> + +<template> + <div + class="avatar s40 identicon" + :style="identiconStyles"> + {{identiconTitle}} + </div> +</template> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index b1db34b9c50..cb133cf7535 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,7 +1,11 @@ <script> import eventHub from '../event_hub'; +import groupIdenticon from './group_identicon.vue'; export default { + components: { + groupIdenticon, + }, props: { group: { type: Object, @@ -92,6 +96,9 @@ export default { hasGroups() { return Object.keys(this.group.subGroups).length > 0; }, + hasAvatar() { + return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1; + }, }, }; </script> @@ -194,9 +201,15 @@ export default { <a :href="group.groupPath"> <img + v-if="hasAvatar" class="avatar s40" :src="group.avatarUrl" /> + <group-identicon + v-else + :entity-id=group.id + :entity-name="group.name" + /> </a> </div> <div diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index e46c0e90255..c39ffdb2e0f 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ /* global IssuableIndex */ /* global Flash */ +import _ from 'underscore'; export default { init({ container, form, issues, prefixId } = {}) { diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 5c96646def8..ece0220c927 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ /* global IssuableIndex */ - +import _ from 'underscore'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 8d7d3d73571..7d7f91227f9 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,8 +1,9 @@ /* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */ /* global Issuable */ /* global ListLabel */ - +import _ from 'underscore'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import DropdownUtils from './filtered_search/dropdown_utils'; (function() { this.LabelsSelect = (function() { @@ -218,18 +219,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; } } if (label.duplicate) { - spacing = 100 / label.color.length; - // Reduce the colors to 4 - label.color = label.color.filter(function(color, i) { - return i < 4; - }); - color = _.map(label.color, function(color, i) { - var percentFirst, percentSecond; - percentFirst = Math.floor(spacing * i); - percentSecond = Math.floor(spacing * (i + 1)); - return color + " " + percentFirst + "%," + color + " " + percentSecond + "% "; - }).join(','); - color = "linear-gradient(" + color + ")"; + color = gl.DropdownUtils.duplicateLabelColor(label.color); } else { if (label.color != null) { diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 6186ffe20b3..5c1ba416a03 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; import NewNavSidebar from './new_sidebar'; +import initFlyOutNav from './fly_out_nav'; (function() { var hideEndFade; @@ -58,6 +59,8 @@ import NewNavSidebar from './new_sidebar'; if (Cookies.get('new_nav') === 'true') { const newNavSidebar = new NewNavSidebar(); newNavSidebar.bindEvents(); + + initFlyOutNav(); } $(window).on('scroll', _.throttle(applyScrollNavClass, 100)); diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index 7477b5a5214..629d8f44e18 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -6,6 +6,10 @@ class AjaxCache extends Cache { this.pendingRequests = { }; } + override(endpoint, data) { + this.internalStorage[endpoint] = data; + } + retrieve(endpoint, forceRetrieve) { if (this.hasData(endpoint) && !forceRetrieve) { return Promise.resolve(this.get(endpoint)); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 122ec138c59..e916724b666 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -86,8 +86,9 @@ // This is required to handle non-unicode characters in hash hash = decodeURIComponent(hash); - var fixedTabs = document.querySelector('.js-tabs-affix'); - var fixedNav = document.querySelector('.navbar-gitlab'); + const fixedTabs = document.querySelector('.js-tabs-affix'); + const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); + const fixedNav = document.querySelector('.navbar-gitlab'); var adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -104,6 +105,11 @@ if (fixedTabs) { adjustment -= fixedTabs.offsetHeight; } + + if (fixedDiffStats) { + adjustment -= fixedDiffStats.offsetHeight; + } + window.scrollBy(0, adjustment); } }; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js index ae397212e55..716aefbfcb7 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ b/app/assets/javascripts/lib/utils/pretty_time.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + (() => { /* * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints, diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js new file mode 100644 index 00000000000..43a808b6ab3 --- /dev/null +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -0,0 +1,23 @@ +export const isSticky = (el, scrollY, stickyTop) => { + const top = el.offsetTop - scrollY; + + if (top === stickyTop) { + el.classList.add('is-stuck'); + } else { + el.classList.remove('is-stuck'); + } +}; + +export default (el) => { + if (!el) return; + + const computedStyle = window.getComputedStyle(el); + + if (!/sticky/.test(computedStyle.position)) return; + + const stickyTop = parseInt(computedStyle.top, 10); + + document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), { + passive: true, + }); +}; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index cd45091c211..42092a34c2f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -16,9 +16,6 @@ import 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; import 'vendor/fuzzaldrin-plus'; -// extensions -import './extensions/array'; - // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; @@ -36,9 +33,6 @@ import './shortcuts_find_file'; import './shortcuts_issuable'; import './shortcuts_network'; -// behaviors -import './behaviors/'; - // templates import './templates/issuable_template_selector'; import './templates/issuable_template_selectors'; @@ -56,6 +50,9 @@ import './lib/utils/pretty_time'; import './lib/utils/text_utility'; import './lib/utils/url_utility'; +// behaviors +import './behaviors/'; + // u2f import './u2f/authenticate'; import './u2f/error'; @@ -86,7 +83,6 @@ import './copy_as_gfm'; import './copy_to_clipboard'; import './create_label'; import './diff'; -import './dispatcher'; import './dropzone_input'; import './due_date_select'; import './files_comment_button'; @@ -150,9 +146,13 @@ import './subscription'; import './subscription_select'; import './syntax_highlight'; +import './dispatcher'; + // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); +Dropzone.autoDiscover = false; + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 7840f05a8ae..4ffd71d9de5 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,6 +7,7 @@ import Cookies from 'js-cookie'; import './breakpoints'; import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; +import stickyMonitor from './lib/utils/sticky'; /* eslint-disable max-len */ // MergeRequestTabs @@ -266,6 +267,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; const $container = $('#diffs'); $container.html(data.html); + this.initChangesDropdown(); + + stickyMonitor(document.querySelector('.js-diff-files-changed')); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } @@ -314,6 +319,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; }); } + initChangesDropdown() { + $('.js-diff-stats-dropdown').glDropdown({ + filterable: true, + remoteFilter: false, + }); + } + // Show or hide the loading spinner // // status - Boolean, true to show, false to hide diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 6756ab0b3aa..04579058688 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ +import _ from 'underscore'; (function() { this.MilestoneSelect = (function() { 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/notes.js b/app/assets/javascripts/notes.js index dfa07a2def4..b38a6abc8d1 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -11,6 +11,7 @@ newline-per-chained-call, no-useless-escape, class-methods-use-this */ /* global mrRefreshWidgetUrl */ import $ from 'jquery'; +import _ from 'underscore'; import Cookies from 'js-cookie'; import autosize from 'vendor/autosize'; import Dropzone from 'dropzone'; 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/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue index ce46b3fa3fa..b5d85299cf8 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -1,4 +1,6 @@ <script> + import _ from 'underscore'; + export default { props: { initialCronInterval: { 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/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index cf1566eeb87..291ae24aa68 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -1,6 +1,7 @@ /* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ -import 'vendor/cropper'; +import 'cropper'; +import _ from 'underscore'; ((global) => { // Matches everything but the file name diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index a3f7d69b98d..6e1744e8e72 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -10,14 +10,19 @@ import Cookies from 'js-cookie'; const $projectCloneField = $('#project_clone'); const $cloneBtnText = $('a.clone-dropdown-btn span'); + const selectedCloneOption = $cloneBtnText.text().trim(); + if (selectedCloneOption.length > 0) { + $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); + } + $('a', $cloneOptions).on('click', (e) => { const $this = $(e.currentTarget); const url = $this.attr('href'); e.preventDefault(); - $('.active', $cloneOptions).not($this).removeClass('active'); - $this.toggleClass('active'); + $('.is-active', $cloneOptions).not($this).removeClass('is-active'); + $this.toggleClass('is-active'); $projectCloneField.val(url); $cloneBtnText.text($this.text()); diff --git a/app/assets/javascripts/project_edit.js b/app/assets/javascripts/project_edit.js index d7d284b6c86..7572fec15e0 100644 --- a/app/assets/javascripts/project_edit.js +++ b/app/assets/javascripts/project_edit.js @@ -1,6 +1,6 @@ export default function setupProjectEdit() { const $transferForm = $('.js-project-transfer-form'); - const $selectNamespace = $transferForm.find('.select2'); + const $selectNamespace = $transferForm.find('select.select2'); $selectNamespace.on('change', () => { $transferForm.find(':submit').prop('disabled', !$selectNamespace.val()); 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 2091b275c3d..985521aef34 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,38 @@ -document.addEventListener('DOMContentLoaded', () => { - const importBtnTooltip = 'Please enter a valid project name.'; - const $importBtnWrapper = $('.import_gitlab_project'); +let hasUserDefinedProjectPath = false; + +const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { + if (hasUserDefinedProjectPath) { + return; + } + + let importUrl = $projectImportUrl.val().trim(); + if (importUrl.length === 0) { + return; + } + + /* + \/?: remove trailing slash + (\.git\/?)?: remove trailing .git (with optional trailing slash) + (\?.*)?: remove query string + (#.*)?: remove fragment identifier + */ + importUrl = importUrl.replace(/\/?(\.git\/?)?(\?.*)?(#.*)?$/, ''); + + // extract everything after the last slash + const pathMatch = /\/([^/]+)$/.exec(importUrl); + if (pathMatch) { + $projectPath.val(pathMatch[1]); + } +}; + +const bindEvents = () => { + const $newProjectForm = $('#new_project'); + const $projectImportUrl = $('#project_import_url'); + const $projectPath = $('#project_path'); + + if ($newProjectForm.length !== 1) { + return; + } $('.how_to_import_link').on('click', (e) => { e.preventDefault(); @@ -13,31 +45,23 @@ document.addEventListener('DOMContentLoaded', () => { $('.btn_import_gitlab_project').on('click', () => { const importHref = $('a.btn_import_gitlab_project').attr('href'); - $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$('#project_path').val()}`); + $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); }); - $('.btn_import_gitlab_project').attr('disabled', !$('#project_path').val().trim().length); - $importBtnWrapper.attr('title', importBtnTooltip); - - $('#new_project').on('submit', () => { - const $path = $('#project_path'); - $path.val($path.val().trim()); + $newProjectForm.on('submit', () => { + $projectPath.val($projectPath.val().trim()); }); - $('#project_path').on('keyup', () => { - if ($('#project_path').val().trim().length) { - $('.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'); - } + $projectPath.on('keyup', () => { + hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; }); - $('#project_import_url').disable(); - $('.import_git').on('click', () => { - const $projectImportUrl = $('#project_import_url'); - $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')); - }); -}); + $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); +}; + +document.addEventListener('DOMContentLoaded', bindEvents); + +export default { + bindEvents, + deriveProjectPathFromUrl, +}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js index cc0b2ebe071..678882a8d2c 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export default class ProtectedBranchDropdown { /** * @param {Object} options containing diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js index 9d045886262..a0224213aa0 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export default class ProtectedTagDropdown { /** * @param {Object} options containing diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index d8f1fe10b26..fa958d75fa4 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ +import _ from 'underscore'; import Cookies from 'js-cookie'; import SidebarHeightManager from './sidebar_height_manager'; diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 51448252c0f..0be141eb5f9 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -3,6 +3,7 @@ /* global ShortcutsNavigation */ /* global sidebar */ +import _ from 'underscore'; import 'mousetrap'; import './shortcuts_navigation'; @@ -58,7 +59,7 @@ import './shortcuts_navigation'; }); // If replyField already has some content, add a newline before our quote separator = replyField.val().trim() !== "" && "\n\n" || ''; - replyField.val(function(_, current) { + replyField.val(function(a, current) { return current + separator + quote.join('') + "\n"; }); diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js index 650e935b116..2d682215cf8 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + import '~/smart_interval'; import timeTracker from './time_tracker'; diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js index 022415f22b2..df19d7305f8 100644 --- a/app/assets/javascripts/sidebar_height_manager.js +++ b/app/assets/javascripts/sidebar_height_manager.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export default { init() { if (!this.initialized) { @@ -30,4 +32,3 @@ export default { } }, }; - diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index bba8b5abbb4..a606852c22c 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -52,6 +52,7 @@ export default class Todos { } updateRowStateClicked(e) { + e.stopPropagation(); e.preventDefault(); const target = e.target; @@ -92,6 +93,7 @@ export default class Todos { } updateAllStateClicked(e) { + e.stopPropagation(); e.preventDefault(); const target = e.currentTarget; @@ -142,6 +144,7 @@ export default class Todos { if (gl.utils.isMetaClick(e)) { const windowTarget = '_blank'; const selected = e.target; + e.stopPropagation(); e.preventDefault(); if (selected.tagName === 'IMG') { diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/two_factor_auth.js new file mode 100644 index 00000000000..d26f61562a5 --- /dev/null +++ b/app/assets/javascripts/two_factor_auth.js @@ -0,0 +1,13 @@ +/* global U2FRegister */ +document.addEventListener('DOMContentLoaded', () => { + const twoFactorNode = document.querySelector('.js-two-factor-auth'); + const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; + if (skippable) { + const button = `<a class="btn btn-xs btn-warning pull-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const flashAlert = document.querySelector('.flash-alert .container-fluid'); + if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); + } + + const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f); + u2fRegister.start(); +}); diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index cd5280948fd..8821b22477f 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -3,6 +3,8 @@ /* global U2FError */ /* global U2FUtil */ +import _ from 'underscore'; + // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 1234d17b8fd..3a2534d553b 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -3,6 +3,8 @@ /* global U2FError */ /* global U2FUtil */ +import _ from 'underscore'; + // Register U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> registered -> POST to server diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js new file mode 100644 index 00000000000..f503076715c --- /dev/null +++ b/app/assets/javascripts/ui_development_kit.js @@ -0,0 +1,22 @@ +import Api from './api'; + +document.addEventListener('DOMContentLoaded', () => { + $('#js-project-dropdown').glDropdown({ + data: (term, callback) => { + Api.projects(term, { + order_by: 'last_activity_at', + }, (data) => { + callback(data); + }); + }, + text: project => (project.name_with_namespace || project.name), + selectable: true, + fieldName: 'author_id', + filterable: true, + search: { + fields: ['name_with_namespace'], + }, + id: data => data.id, + isSelected: data => (data.id === 2), + }); +}); diff --git a/app/assets/javascripts/username_validator.js b/app/assets/javascripts/username_validator.js index a348d69153c..bb34d5d2008 100644 --- a/app/assets/javascripts/username_validator.js +++ b/app/assets/javascripts/username_validator.js @@ -1,5 +1,7 @@ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ +import _ from 'underscore'; + const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; const successInputClass = 'gl-field-success-outline'; diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index f091e319f44..5e947769f8a 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import d3 from 'd3'; const LOADING_HTML = ` @@ -6,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); @@ -21,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 = ''; @@ -36,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/users_select.js b/app/assets/javascripts/users_select.js index 5728afb4c59..16ebf5916dc 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ /* global Issuable */ /* global emitSidebarEvent */ +import _ from 'underscore'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; 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 index 0bd31731a0b..a12f418e1af 100644 --- 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 @@ -1,24 +1,29 @@ +import statusIcon from '../mr_widget_status_icon'; + export default { name: 'MRWidgetLocked', props: { mr: { type: Object, required: true }, }, + components: { + statusIcon, + }, 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 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, during which time it is locked and cannot be closed + </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_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_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..546a3f625c7 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 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..577d77f09a6 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 @@ -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, @@ -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/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/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 3f934403147..02e0ba74158 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -574,6 +574,7 @@ .dropdown-input-field, .default-dropdown-input { + display: block; width: 100%; min-height: 30px; padding: 0 7px; @@ -722,3 +723,57 @@ @include set-invisible; overflow: hidden; } + +// TODO: change global style and remove mixin +@mixin new-style-dropdown { + .dropdown-menu, + .dropdown-menu-nav { + .divider { + margin: 6px 0; + } + + li { + padding: 0 1px; + + &.dropdown-header { + padding: 8px 16px; + } + + a { + border-radius: 0; + padding: 8px 16px; + + &.is-focused, + &:hover, + &:active, + &:focus { + background-color: $gray-darker; + } + + &.is-active { + font-weight: inherit; + + &::before { + top: 16px; + } + } + } + } + + &.dropdown-menu-selectable { + li { + a { + padding: 8px 40px; + + &.is-active::before { + left: 16px; + } + } + } + } + } + + .dropdown-menu-align-right { + margin-top: 2px; + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 1c4238bc564..d9f92e93160 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -4,6 +4,8 @@ */ header { + @include new-style-dropdown; + transition: padding $sidebar-transition-duration; &.navbar-empty { @@ -24,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; @@ -313,25 +315,6 @@ header { .impersonation i { color: $red-500; } - - // TODO: fallback to global style - .dropdown-menu, - .dropdown-menu-nav { - li { - padding: 0 1px; - - a { - border-radius: 0; - padding: 8px 16px; - - &:hover, - &:active, - &:focus { - background-color: $gray-darker; - } - } - } - } } .with-performance-bar header.navbar-gitlab { @@ -342,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..0d8827bec11 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -109,16 +109,8 @@ 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 { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 868e65a8f46..ab754f4a492 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -369,6 +369,10 @@ ul.indent-list { background-color: $row-hover; cursor: pointer; } + + .avatar-container > a { + width: 100%; + } } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index a2de4598167..fcd4c72b430 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -185,3 +185,28 @@ text-overflow: ellipsis; } } + +// TODO: fallback to global style +.atwho-view { + .atwho-view-ul { + padding: 8px 1px; + + li { + padding: 8px 16px; + border: 0; + + &.cur { + background-color: $gray-darker; + color: $gl-text-color; + + small { + color: inherit; + } + } + + strong { + color: $gl-text-color; + } + } + } +} 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..7a1a89cd2f9 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -206,7 +206,6 @@ $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); $performance-bar-height: 35px; - /* * Common component specific colors */ @@ -316,6 +315,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); @@ -624,3 +629,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 54f3e8d882c..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; } @@ -143,10 +129,19 @@ $new-sidebar-width: 220px; white-space: nowrap; a { - display: block; + display: flex; + align-items: center; padding: 12px 16px; color: $inactive-color; } + + svg { + fill: $inactive-color; + } + } + + .nav-item-name { + flex: 1; } li.active { @@ -156,11 +151,25 @@ $new-sidebar-width: 220px; color: $active-color; font-weight: 700; } + + svg { + fill: $active-color; + } } @media (max-width: $screen-xs-max) { left: (-$new-sidebar-width); } + + .nav-icon-container { + display: flex; + margin-right: 8px; + + svg { + height: 16px; + width: 16px; + } + } } .with-performance-bar .nav-sidebar { @@ -173,7 +182,7 @@ $new-sidebar-width: 220px; > li { a { - padding: 8px 16px 8px 24px; + padding: 8px 16px 8px 50px; &:hover, &:focus { @@ -196,9 +205,86 @@ $new-sidebar-width: 220px; } .sidebar-top-level-items { + margin-bottom: 60px; + > li { + > a { + @media (min-width: $screen-sm-min) { + margin-right: 2px; + } + + &:hover { + color: $gl-text-color; + } + } + + &:not(.active) { + > a { + margin-left: 1px; + margin-right: 2px; + } + + .sidebar-sub-level-items { + @media (min-width: $screen-sm-min) { + position: fixed; + top: 0; + left: $new-sidebar-width; + width: 150px; + margin-top: -1px; + padding: 8px 1px; + background-color: $white-light; + box-shadow: 2px 1px 3px $dropdown-shadow-color; + border: 1px solid $gray-darker; + border-left: 0; + border-radius: 0 3px 3px 0; + + &::before { + content: ""; + position: absolute; + top: -30px; + bottom: -30px; + left: 0; + right: -30px; + z-index: -1; + } + + &::after { + content: ""; + position: absolute; + top: 44px; + left: -30px; + right: 35px; + bottom: 0; + height: 100%; + max-height: 150px; + z-index: -1; + transform: skew(33deg); + } + + &.is-above { + margin-top: 1px; + + &::after { + top: auto; + bottom: 44px; + transform: skew(-30deg); + } + } + + a { + padding: 8px 16px; + color: $gl-text-color; + + &:hover, + &:focus { + background-color: $gray-darker; + } + } + } + } + } + .badge { - float: right; background-color: $inactive-badge-background; color: $inactive-color; } @@ -206,6 +292,10 @@ $new-sidebar-width: 220px; &.active { background: $active-background; + > a { + margin-left: 4px; + } + .badge { color: $active-color; font-weight: 600; @@ -216,18 +306,103 @@ $new-sidebar-width: 220px; } } - > a:hover { - background-color: $hover-background; - color: $hover-color; + &:not(.active):hover > a, + > a:hover, + &.is-over > a { + background-color: $white-light; + } + } +} + - .badge { - background-color: $indigo-500; - color: $hover-color; +// 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; @@ -247,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/builds.scss b/app/assets/stylesheets/pages/builds.scss index acf3719e9d2..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; } @@ -311,9 +329,7 @@ } .dropdown-menu { - right: $gl-padding; - left: $gl-padding; - width: auto; + margin-top: -$gl-padding; } svg { @@ -328,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/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index eeb90759f10..6753eb08285 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,4 +1,6 @@ #cycle-analytics { + @include new-style-dropdown; + max-width: 1000px; margin: 24px auto 0; position: relative; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 398fd4444ea..da77346d8b2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -395,12 +395,11 @@ background-color: transparent; border: 0; color: $gl-link-color; - transition: color 0.1s linear; + font-weight: 600; &:hover, &:focus { outline: none; - text-decoration: underline; color: $gl-link-hover-color; } } @@ -559,3 +558,68 @@ outline: 0; } } + +.diff-files-changed { + .commit-stat-summary { + @include new-style-dropdown; + z-index: -1; + + @media (min-width: $screen-sm-min) { + margin-left: -$gl-padding; + padding-left: $gl-padding; + background-color: $white-light; + } + } + + @media (min-width: $screen-sm-min) { + position: -webkit-sticky; + position: sticky; + top: 84px; + background-color: $white-light; + z-index: 190; + + + .files, + + .alert { + margin-top: 1px; + } + + &:not(.is-stuck) .diff-stats-additions-deletions-collapsed { + display: none; + } + + &.is-stuck { + padding-top: 0; + padding-bottom: 0; + border-bottom: 1px solid $white-dark; + transform: translateY(16px); + + .diff-stats-additions-deletions-expanded, + .inline-parallel-buttons { + display: none; + } + + + .files, + + .alert { + margin-top: 30px; + } + } + } +} + +.diff-file-changes { + width: 450px; + z-index: 150; + + @media (min-width: $screen-sm-min) { + left: $gl-padding; + } + + a { + padding-top: 8px; + padding-bottom: 8px; + } +} + +.diff-file-changes-path { + @include str-truncated(78%); +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6da14320914..88343bd0113 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -328,9 +328,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 4693b2434c7..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,20 +604,16 @@ } .merged-buttons { - margin-top: 20px; - .btn { float: left; - - &:not(:last-child) { - margin-right: 10px; - } } } .mr-version-controls { + position: relative; background: $gray-light; color: $gl-text-color; + z-index: 199; .mr-version-menus-container { display: -webkit-flex; @@ -801,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/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 b3a90dff89a..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 { @@ -282,6 +282,8 @@ } .project-repo-buttons { + @include new-style-dropdown; + .project-action-button .dropdown-menu { max-height: 250px; overflow-y: auto; @@ -456,6 +458,7 @@ a.deploy-project-label { } } +.project-template, .project-import { .form-group { margin-bottom: 5px; @@ -470,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 { @@ -480,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/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 e0f46172769..44ab07a4367 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,4 +1,5 @@ .tree-holder { + @include new-style-dropdown; .nav-block { margin: 10px 0; @@ -202,28 +203,6 @@ } } } - - // TODO: fallback to global style - .dropdown-menu:not(.dropdown-menu-selectable) { - li { - padding: 0 1px; - - &.dropdown-header { - padding: 8px 16px; - } - - a { - border-radius: 0; - padding: 8px 16px; - - &:hover, - &:active, - &:focus { - background-color: $gray-darker; - } - } - } - } } .blob-commit-info { 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; } diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index caf4c138da8..65a17828feb 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,5 +1,12 @@ class Admin::HealthCheckController < Admin::ApplicationController def show @errors = HealthCheck::Utils.process_checks(['standard']) + @failing_storage_statuses = Gitlab::Git::Storage::Health.for_failing_storages + end + + def reset_storage_health + Gitlab::Git::Storage::CircuitBreaker.reset_all! + redirect_to admin_health_check_path, + notice: _('Git storage health information has been reset') end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d14b1dbecf6..5b448008a1b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -52,6 +52,15 @@ class ApplicationController < ActionController::Base head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window end + rescue_from Gitlab::Git::Storage::Inaccessible, GRPC::Unavailable, Gitlab::Git::CommandError do |exception| + Raven.capture_exception(exception) if sentry_enabled? + log_exception(exception) + + headers['Retry-After'] = exception.retry_after if exception.respond_to?(:retry_after) + + render_503 + end + def redirect_back_or_default(default: root_path, options: {}) redirect_to request.referer.present? ? :back : default, options end @@ -152,6 +161,19 @@ class ApplicationController < ActionController::Base head :unprocessable_entity end + def render_503 + respond_to do |format| + format.html do + render( + file: Rails.root.join("public", "503"), + layout: false, + status: :service_unavailable + ) + end + format.any { head :service_unavailable } + end + end + def no_cache_headers response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" response.headers["Pragma"] = "no-cache" diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 91c1e4dff79..74fe45e1ff6 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -45,8 +45,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def load_projects(finder_params) - ProjectsFinder.new(params: finder_params, current_user: current_user) - .execute.includes(:route, namespace: :route) + ProjectsFinder + .new(params: finder_params, current_user: current_user) + .execute + .includes(:route, :creator, namespace: :route) end def load_events diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 59e5b5e4775..a8b2b93b458 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -13,7 +13,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy - TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user) + TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) respond_to do |format| format.html do @@ -37,7 +37,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def restore - TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user) + TodoService.new.mark_todos_as_pending_by_ids(params[:id], current_user) render json: todos_counts end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 36d246d185b..510813846a4 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -12,15 +12,7 @@ class Import::GitlabProjectsController < Import::BaseController return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) end - import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename) - - FileUtils.mkdir_p(File.dirname(import_upload_path)) - FileUtils.copy_entry(project_params[:file].path, import_upload_path) - - @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id], - current_user, - import_upload_path, - project_params[:path]).execute + @project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute if @project.saved? redirect_to( diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 323d5d26eb6..b4213574561 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -34,12 +34,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if @user.two_factor_enabled? prompt_for_two_factor(@user) else - log_audit_event(@user, with: :ldap) + log_audit_event(@user, with: oauth['provider']) sign_in_and_redirect(@user) end else - flash[:alert] = "Access denied for your LDAP account." - redirect_to new_user_session_path + fail_ldap_login end end @@ -123,9 +122,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController sign_in_and_redirect(@user) end else - error_message = @user.errors.full_messages.to_sentence - - return redirect_to omniauth_error_path(oauth['provider'], error: error_message) + fail_login end end @@ -145,6 +142,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def oauth @oauth ||= request.env['omniauth.auth'] end + + def fail_login + error_message = @user.errors.full_messages.to_sentence + + return redirect_to omniauth_error_path(oauth['provider'], error: error_message) + end + + def fail_ldap_login + flash[:alert] = 'Access denied for your LDAP account.' + + redirect_to new_user_session_path + end def log_audit_event(user, options = {}) AuditEventService.new(user, user, options) diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index da9b789d617..653e7bc7e40 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -66,7 +66,8 @@ module Projects end def filter_params - params.merge(board_id: params[:board_id], id: params[:list_id]).compact + params.merge(board_id: params[:board_id], id: params[:list_id]) + .reject { |_, value| value.nil? } end def move_params diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 57372f9e79d..475d4c86294 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -43,23 +43,7 @@ class Projects::GraphsController < Projects::ApplicationController end def get_languages - @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages - total = @languages.map(&:last).sum - - @languages = @languages.map do |language| - name, share = language - color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}" - { - value: (share.to_f * 100 / total).round(2), - label: name, - color: color, - highlight: color - } - end - - @languages.sort! do |x, y| - y[:value] <=> x[:value] - end + @languages = @project.repository.languages end def fetch_graph diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2d7cbd4614e..0bffae6decd 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -324,6 +324,7 @@ class ProjectsController < Projects::ApplicationController :runners_token, :tag_list, :visibility_level, + :template_name, project_feature_attributes: %i[ builds_access_level diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3fe37c75381..b276116f0c6 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -95,9 +95,18 @@ class TodosFinder @project end + def project_ids(items) + ids = items.except(:order).select(:project_id) + if Gitlab::Database.mysql? + # To make UPDATE work on MySQL, wrap it in a SELECT with an alias + ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t") + end + + ids + end + def projects(items) - item_project_ids = items.reorder(nil).select(:project_id) - ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute + ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute end def type? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 14dc9bd9d62..0cfd7822d05 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -305,4 +305,8 @@ module ApplicationHelper def show_new_nav? cookies["new_nav"] == "true" end + + def collapsed_sidebar? + cookies["sidebar_collapsed"] == "true" + end end diff --git a/app/helpers/defer_script_tag_helper.rb b/app/helpers/defer_script_tag_helper.rb new file mode 100644 index 00000000000..e1567556e5e --- /dev/null +++ b/app/helpers/defer_script_tag_helper.rb @@ -0,0 +1,6 @@ +module DeferScriptTagHelper + # Override the default ActionView `javascript_include_tag` helper to support page specific deferred loading + def javascript_include_tag(*sources) + super(*sources, defer: true) + end +end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 91ddd73fac1..087f7f88fb5 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -148,6 +148,24 @@ module DiffHelper options end + def diff_file_changed_icon(diff_file) + if diff_file.deleted_file? || diff_file.renamed_file? + "minus" + elsif diff_file.new_file? + "plus" + else + "adjust" + end + end + + def diff_file_changed_icon_color(diff_file) + if diff_file.deleted_file? + "cred" + elsif diff_file.new_file? + "cgreen" + end + end + private def diff_btn(title, name, selected) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 1f7db9b2eb8..d4a91e533c1 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -47,14 +47,6 @@ module GitlabRoutingHelper project_pipeline_path(pipeline.project, pipeline.id, *args) end - def milestone_path(entity, *args) - if entity.is_group_milestone? - group_milestone_path(entity.group, entity, *args) - elsif entity.is_project_milestone? - project_milestone_path(entity.project, entity, *args) - end - end - def issue_url(entity, *args) project_issue_url(entity.project, entity, *args) end @@ -67,14 +59,6 @@ module GitlabRoutingHelper project_pipeline_url(pipeline.project, pipeline.id, *args) end - def milestone_url(entity, *args) - if entity.is_group_milestone? - group_milestone_url(entity.group, entity, *args) - elsif entity.is_project_milestone? - project_milestone_url(entity.project, entity, *args) - end - end - def pipeline_job_url(pipeline, build, *args) project_job_url(pipeline.project, build.id, *args) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 78cf7b26a31..c31023f2d9a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -40,7 +40,7 @@ module MergeRequestsHelper def merge_path_description(merge_request, separator) if merge_request.for_fork? - "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}" + "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.full_path}:#{@merge_request.target_branch}" else "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}" end diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb new file mode 100644 index 00000000000..766d5262018 --- /dev/null +++ b/app/helpers/milestones_routing_helper.rb @@ -0,0 +1,17 @@ +module MilestonesRoutingHelper + def milestone_path(milestone, *args) + if milestone.is_group_milestone? + group_milestone_path(milestone.group, milestone, *args) + elsif milestone.is_project_milestone? + project_milestone_path(milestone.project, milestone, *args) + end + end + + def milestone_url(milestone, *args) + if milestone.is_group_milestone? + group_milestone_url(milestone.group, milestone, *args) + elsif milestone.is_project_milestone? + project_milestone_url(milestone.project, milestone, *args) + end + end +end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index b1205b8529b..b63b3b70903 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -2,6 +2,7 @@ module NavHelper def page_with_sidebar_class class_name = page_gutter_class class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar class_name end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9a8d296d514..a268413e84f 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -225,6 +225,26 @@ module ProjectsHelper end end + # Returns true if any projects are present. + # + # If the relation has a LIMIT applied we'll cast the relation to an Array + # since repeated any? checks would otherwise result in multiple COUNT queries + # being executed. + # + # If no limit is applied we'll just issue a COUNT since the result set could + # be too large to load into memory. + def any_projects?(projects) + if projects.limit_value + projects.to_a.any? + else + projects.except(:offset).any? + end + end + + def has_projects_or_name?(projects, params) + !!(params[:name] || any_projects?(projects)) + end + private def repo_children_classes(field) @@ -398,7 +418,7 @@ module ProjectsHelper if project import_path = "/Home/Stacks/import" - repo = project.path_with_namespace + repo = project.full_path branch ||= project.default_branch sha ||= project.commit.short_id @@ -458,7 +478,7 @@ module ProjectsHelper def readme_cache_key sha = @project.commit.try(:sha) || 'nil' - [@project.path_with_namespace, sha, "readme"].join('-') + [@project.full_path, sha, "readme"].join('-') end def current_ref diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index fd7ab59ce64..ae0e0aa3cf9 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -127,15 +127,23 @@ module SearchHelper end def search_filter_input_options(type) - { + opts = { id: "filtered-search-#{type}", placeholder: 'Search or filter results...', data: { - 'project-id' => @project.id, - 'username-params' => @users.to_json(only: [:id, :username]), - 'base-endpoint' => project_path(@project) + 'username-params' => @users.to_json(only: [:id, :username]) } } + + if @project.present? + opts[:data]['project-id'] = @project.id + opts[:data]['base-endpoint'] = project_path(@project) + else + # Group context + opts[:data]['base-endpoint'] = group_canonical_path(@group) + end + + opts end # Sanitize a HTML field for search display. Most tags are stripped out and the diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb new file mode 100644 index 00000000000..544c9efb845 --- /dev/null +++ b/app/helpers/storage_health_helper.rb @@ -0,0 +1,37 @@ +module StorageHealthHelper + def failing_storage_health_message(storage_health) + storage_name = content_tag(:strong, h(storage_health.storage_name)) + host_names = h(storage_health.failing_on_hosts.to_sentence) + translation_params = { storage_name: storage_name, + host_names: host_names, + failed_attempts: storage_health.total_failures } + + translation = n_('%{storage_name}: failed storage access attempt on host:', + '%{storage_name}: %{failed_attempts} failed storage access attempts:', + storage_health.total_failures) % translation_params + + translation.html_safe + end + + def message_for_circuit_breaker(circuit_breaker) + maximum_failures = circuit_breaker.failure_count_threshold + current_failures = circuit_breaker.failure_count + permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures + + translation_params = { number_of_failures: current_failures, + maximum_failures: maximum_failures, + number_of_seconds: circuit_breaker.failure_wait_time } + + if permanently_broken + s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ + "retry automatically. Reset storage information when the problem is "\ + "resolved.") % translation_params + elsif circuit_breaker.circuit_broken? + _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ + "block access for %{number_of_seconds} seconds.") % translation_params + else + _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ + "allow access on the next attempt.") % translation_params + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index eaac6fcb548..9efabe3f44e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -165,7 +165,7 @@ class Notify < BaseMailer headers['X-GitLab-Project'] = @project.name headers['X-GitLab-Project-Id'] = @project.id - headers['X-GitLab-Project-Path'] = @project.path_with_namespace + headers['X-GitLab-Project-Path'] = @project.full_path end def add_unsubscription_headers_and_links diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d2abcf30034..ea7331cb27f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -317,7 +317,7 @@ module Ci return @config_processor if defined?(@config_processor) @config_processor ||= begin - Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) + Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path) rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e self.yaml_errors = e.message nil diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index a40148a4394..fde1cc44afa 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -1,6 +1,12 @@ module ProtectedBranchAccess extend ActiveSupport::Concern + ALLOWED_ACCESS_LEVELS ||= [ + Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS + ].freeze + included do include ProtectedRefAccess @@ -9,11 +15,7 @@ module ProtectedBranchAccess delegate :project, to: :protected_branch validates :access_level, presence: true, inclusion: { - in: [ - Gitlab::Access::MASTER, - Gitlab::Access::DEVELOPER, - Gitlab::Access::NO_ACCESS - ] + in: ALLOWED_ACCESS_LEVELS } def self.human_access_levels diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index da803c7f481..10f4be72016 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -25,6 +25,18 @@ module Referable to_reference(from_project) end + def referable_inspect + if respond_to?(:id) + "#<#{self.class.name} id:#{id} #{to_reference(full: true)}>" + else + "#<#{self.class.name} #{to_reference(full: true)}>" + end + end + + def inspect + referable_inspect + end + module ClassMethods # The character that prefixes the actual reference identifier # diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb new file mode 100644 index 00000000000..5ab5c80a2f5 --- /dev/null +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -0,0 +1,102 @@ +module Storage + module LegacyNamespace + extend ActiveSupport::Concern + + def move_dir + if any_project_has_container_registry_tags? + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') + end + + # Move the namespace directory in all storage paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, full_path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') + end + end + + Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) + Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) + + remove_exports! + + # If repositories moved successfully we need to + # send update instructions to users. + # However we cannot allow rollback since we moved namespace dir + # So we basically we mute exceptions in next actions + begin + send_update_instructions + true + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false + end + end + + # Hooks + + # Save the storage paths before the projects are destroyed to use them on after destroy + def prepare_for_destroy + old_repository_storage_paths + end + + private + + def old_repository_storage_paths + @old_repository_storage_paths ||= repository_storage_paths + end + + def repository_storage_paths + # We need to get the storage paths for all the projects, even the ones that are + # pending delete. Unscoping also get rids of the default order, which causes + # problems with SELECT DISTINCT. + Project.unscoped do + all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) + end + end + + def rm_dir + # Remove the namespace directory in all storages paths used by member projects + old_repository_storage_paths.each do |repository_storage_path| + # Move namespace directory into trash. + # We will remove it later async + new_path = "#{full_path}+#{id}+deleted" + + if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path) + Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}") + + # Remove namespace directroy async with delay so + # GitLab has time to remove all projects first + run_after_commit do + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) + end + end + end + + remove_exports! + end + + def remove_exports! + Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) + end + + def export_path + File.join(Gitlab::ImportExport.storage_path, full_path_was) + end + + def full_path_was + if parent + parent.full_path + '/' + path_was + else + path_was + end + end + end +end diff --git a/app/models/concerns/storage/legacy_project.rb b/app/models/concerns/storage/legacy_project.rb new file mode 100644 index 00000000000..815db712285 --- /dev/null +++ b/app/models/concerns/storage/legacy_project.rb @@ -0,0 +1,76 @@ +module Storage + module LegacyProject + extend ActiveSupport::Concern + + def disk_path + full_path + end + + def ensure_storage_path_exist + gitlab_shell.add_namespace(repository_storage_path, namespace.full_path) + end + + def rename_repo + path_was = previous_changes['path'].first + old_path_with_namespace = File.join(namespace.full_path, path_was) + new_path_with_namespace = File.join(namespace.full_path, path) + + Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" + + if has_container_registry_tags? + Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present!" + + # we currently doesn't support renaming repository if it contains images in container registry + raise StandardError.new('Project cannot be renamed, because images are present in its container registry') + end + + expire_caches_before_rename(old_path_with_namespace) + + if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) + # If repository moved successfully we need to send update instructions to users. + # However we cannot allow rollback since we moved repository + # So we basically we mute exceptions in next actions + begin + gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") + send_move_instructions(old_path_with_namespace) + expires_full_path_cache + + @old_path_with_namespace = old_path_with_namespace + + SystemHooksService.new.execute_hooks_for(self, :rename) + + @repository = nil + rescue => e + Rails.logger.error "Exception renaming #{old_path_with_namespace} -> #{new_path_with_namespace}: #{e}" + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false + end + else + Rails.logger.error "Repository could not be renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise StandardError.new('repository cannot be renamed') + end + + Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" + + Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.full_path) + Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.full_path) + end + + def create_repository(force: false) + # Forked import is handled asynchronously + return if forked? && !force + + if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) + repository.after_create + true + else + errors.add(:base, 'Failed to create repository via gitlab-shell') + false + end + end + end +end diff --git a/app/models/concerns/storage/legacy_project_wiki.rb b/app/models/concerns/storage/legacy_project_wiki.rb new file mode 100644 index 00000000000..ff82cb0ffa9 --- /dev/null +++ b/app/models/concerns/storage/legacy_project_wiki.rb @@ -0,0 +1,9 @@ +module Storage + module LegacyProjectWiki + extend ActiveSupport::Concern + + def disk_path + project.disk_path + '.wiki' + end + end +end diff --git a/app/models/concerns/storage/legacy_repository.rb b/app/models/concerns/storage/legacy_repository.rb new file mode 100644 index 00000000000..593749bf019 --- /dev/null +++ b/app/models/concerns/storage/legacy_repository.rb @@ -0,0 +1,7 @@ +module Storage + module LegacyRepository + extend ActiveSupport::Concern + + delegate :disk_path, to: :project + end +end diff --git a/app/models/conversational_development_index/metric.rb b/app/models/conversational_development_index/metric.rb index f42f516f99a..0bee62f954f 100644 --- a/app/models/conversational_development_index/metric.rb +++ b/app/models/conversational_development_index/metric.rb @@ -13,9 +13,7 @@ module ConversationalDevelopmentIndex end def percentage_score(feature) - return 100 if leader_score(feature).zero? - - 100 * instance_score(feature) / leader_score(feature) + self["percentage_#{feature}"] end end end diff --git a/app/models/key.rb b/app/models/key.rb index cb8f10f6d55..49bc26122fa 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -16,8 +16,6 @@ class Key < ActiveRecord::Base presence: true, length: { maximum: 5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } - validates :key, - format: { without: /\n|\r/, message: 'should be a single line' } validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } @@ -31,6 +29,7 @@ class Key < ActiveRecord::Base after_destroy :post_destroy_hook def key=(value) + value&.delete!("\n\r") value.strip! unless value.blank? write_attribute(:key, value) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 81e0776e79c..8ca850b6d96 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -630,7 +630,7 @@ class MergeRequest < ActiveRecord::Base def target_project_path if target_project - target_project.path_with_namespace + target_project.full_path else "(removed)" end @@ -638,7 +638,7 @@ class MergeRequest < ActiveRecord::Base def source_project_path if source_project - source_project.path_with_namespace + source_project.full_path else "(removed)" end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index ec87aee9310..d9d746ccf41 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -85,11 +85,7 @@ class MergeRequestDiff < ActiveRecord::Base def raw_diffs(options = {}) if options[:ignore_whitespace_change] - @diffs_no_whitespace ||= - Gitlab::Git::Compare.new( - repository.raw_repository, - safe_start_commit_sha, - head_commit_sha).diffs(options) + @diffs_no_whitespace ||= compare.diffs(options) else @raw_diffs ||= {} @raw_diffs[options] ||= load_diffs(options) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 48d00764965..01e0d0155a3 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -149,7 +149,9 @@ class Milestone < ActiveRecord::Base end ## - # Returns the String necessary to reference this Milestone in Markdown + # Returns the String necessary to reference this Milestone in Markdown. Group + # milestones only support name references, and do not support cross-project + # references. # # format - Symbol format to use (default: :iid, optional: :name) # @@ -161,12 +163,16 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid, full: false) - return if is_group_milestone? + return if is_group_milestone? && format != :name format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - "#{project.to_reference(from_project, full: full)}#{reference}" + if project + "#{project.to_reference(from_project, full: full)}#{reference}" + else + reference + end end def reference_link_text(from_project = nil) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 0bb04194bdb..6073fb94a3f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -8,6 +8,7 @@ class Namespace < ActiveRecord::Base include Gitlab::VisibilityLevel include Routable include AfterCommitQueue + include Storage::LegacyNamespace # Prevent users from creating unreasonably deep level of nesting. # The number 20 was taken based on maximum nesting level of @@ -41,10 +42,11 @@ class Namespace < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true - after_update :move_dir, if: :path_changed? after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } - # Save the storage paths before the projects are destroyed to use them on after destroy + # Legacy Storage specific hooks + + after_update :move_dir, if: :path_changed? before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir @@ -118,43 +120,6 @@ class Namespace < ActiveRecord::Base owner_name end - def move_dir - if any_project_has_container_registry_tags? - raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') - end - - # Move the namespace directory in all storages paths used by member projects - repository_storage_paths.each do |repository_storage_path| - # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage_path, full_path_was) - - unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) - Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" - - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') - end - end - - Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) - Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) - - remove_exports! - - # If repositories moved successfully we need to - # send update instructions to users. - # However we cannot allow rollback since we moved namespace dir - # So we basically we mute exceptions in next actions - begin - send_update_instructions - rescue - # Returning false does not rollback after_* transaction but gives - # us information about failing some of tasks - false - end - end - def any_project_has_container_registry_tags? all_projects.any?(&:has_container_registry_tags?) end @@ -206,14 +171,6 @@ class Namespace < ActiveRecord::Base parent_id_changed? end - def prepare_for_destroy - old_repository_storage_paths - end - - def old_repository_storage_paths - @old_repository_storage_paths ||= repository_storage_paths - end - # Includes projects from this namespace and projects from all subgroups # that belongs to this namespace def all_projects @@ -232,37 +189,6 @@ class Namespace < ActiveRecord::Base private - def repository_storage_paths - # We need to get the storage paths for all the projects, even the ones that are - # pending delete. Unscoping also get rids of the default order, which causes - # problems with SELECT DISTINCT. - Project.unscoped do - all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) - end - end - - def rm_dir - # Remove the namespace directory in all storages paths used by member projects - old_repository_storage_paths.each do |repository_storage_path| - # Move namespace directory into trash. - # We will remove it later async - new_path = "#{full_path}+#{id}+deleted" - - if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path) - message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\"" - Gitlab::AppLogger.info message - - # Remove namespace directroy async with delay so - # GitLab has time to remove all projects first - run_after_commit do - GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) - end - end - end - - remove_exports! - end - def refresh_access_of_projects_invited_groups Group .joins(project_group_links: :project) @@ -270,22 +196,6 @@ class Namespace < ActiveRecord::Base .find_each(&:refresh_members_authorized_projects) end - def remove_exports! - Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) - end - - def export_path - File.join(Gitlab::ImportExport.storage_path, full_path_was) - end - - def full_path_was - if parent - parent.full_path + '/' + path_was - else - path_was - end - end - def nesting_level_allowed if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED errors.add(:parent_id, "has too deep level of nesting") diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb new file mode 100644 index 00000000000..418b42d8f1d --- /dev/null +++ b/app/models/notification_recipient.rb @@ -0,0 +1,125 @@ +class NotificationRecipient + attr_reader :user, :type + def initialize( + user, type, + custom_action: nil, + target: nil, + acting_user: nil, + project: nil + ) + @custom_action = custom_action + @acting_user = acting_user + @target = target + @project = project || @target&.project + @user = user + @type = type + end + + def notification_setting + @notification_setting ||= find_notification_setting + end + + def raw_notification_level + notification_setting&.level&.to_sym + end + + def notification_level + # custom is treated the same as watch if it's enabled - otherwise it's + # set to :custom, meaning to send exactly when our type is :participating + # or :mention. + @notification_level ||= + case raw_notification_level + when :custom + if @custom_action && notification_setting&.event_enabled?(@custom_action) + :watch + else + :custom + end + else + raw_notification_level + end + end + + def notifiable? + return false unless has_access? + return false if own_activity? + + return true if @type == :subscription + + return false if notification_level.nil? || notification_level == :disabled + + return %i[participating mention].include?(@type) if notification_level == :custom + + return false if %i[watch participating].include?(notification_level) && excluded_watcher_action? + + return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[@type] + + return false if unsubscribed? + + true + end + + def unsubscribed? + return false unless @target + return false unless @target.respond_to?(:subscriptions) + + subscription = @target.subscriptions.find_by_user_id(@user.id) + subscription && !subscription.subscribed + end + + def own_activity? + return false unless @acting_user + return false if @acting_user.notified_of_own_activity? + + user == @acting_user + end + + def has_access? + DeclarativePolicy.subject_scope do + return false unless user.can?(:receive_notifications) + return false if @project && !user.can?(:read_project, @project) + + return true unless read_ability + return true unless DeclarativePolicy.has_policy?(@target) + + user.can?(read_ability, @target) + end + end + + def excluded_watcher_action? + return false unless @custom_action + return false if raw_notification_level == :custom + + NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action) + end + + private + + def read_ability + return @read_ability if instance_variable_defined?(:@read_ability) + + @read_ability = + case @target + when Issuable + :"read_#{@target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + when ActiveRecord::Base + :"read_#{@target.class.model_name.name.underscore}" + else + nil + end + end + + def find_notification_setting + project_setting = @project && user.notification_settings_for(@project) + + return project_setting unless project_setting.nil? || project_setting.global? + + group_setting = @project&.group && user.notification_settings_for(@project.group) + + return group_setting unless group_setting.nil? || group_setting.global? + + user.global_notification_setting + end +end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 81844b1e2ca..9b1cac64c44 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,4 +1,8 @@ class NotificationSetting < ActiveRecord::Base + include IgnorableColumn + + ignore_column :events + enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 } default_value_for :level, NotificationSetting.levels[:global] @@ -41,9 +45,6 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ].freeze - store :events, coder: JSON - before_save :convert_events - def self.find_or_create_for(source) setting = find_or_initialize_by(source: source) @@ -54,42 +55,17 @@ class NotificationSetting < ActiveRecord::Base setting end - # 1. Check if this event has a value stored in its database column. - # 2. If it does, return that value. - # 3. If it doesn't (the value is nil), return the value from the serialized - # JSON hash in `events`. - (EMAIL_EVENTS - [:failed_pipeline]).each do |event| - define_method(event) do - bool = super() - - bool.nil? ? !!events[event] : bool - end - - alias_method :"#{event}?", event - end - # Allow people to receive failed pipeline notifications if they already have # custom notifications enabled, as these are more like mentions than the other # custom settings. def failed_pipeline bool = super - bool = events[:failed_pipeline] if bool.nil? bool.nil? || bool end alias_method :failed_pipeline?, :failed_pipeline def event_enabled?(event) - respond_to?(event) && public_send(event) - end - - def convert_events - return if events_before_type_cast.nil? - - EMAIL_EVENTS.each do |event| - write_attribute(event, public_send(event)) - end - - write_attribute(:events, nil) + respond_to?(event) && !!public_send(event) end end diff --git a/app/models/project.rb b/app/models/project.rb index d827bfaa806..e7baba2ef08 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,6 +17,7 @@ class Project < ActiveRecord::Base include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Routable + include Storage::LegacyProject extend Gitlab::ConfigHelper @@ -43,9 +44,8 @@ class Project < ActiveRecord::Base default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :only_allow_merge_if_all_discussions_are_resolved, false - after_create :ensure_dir_exist + after_create :ensure_storage_path_exist after_create :create_project_feature, unless: :project_feature - after_save :ensure_dir_exist, if: :namespace_id_changed? after_save :update_project_statistics, if: :namespace_id_changed? # set last_activity_at to the same as created_at @@ -67,10 +67,15 @@ class Project < ActiveRecord::Base after_validation :check_pending_delete + # Legacy Storage specific hooks + + after_save :ensure_storage_path_exist, if: :namespace_id_changed? + acts_as_taggable attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + attr_accessor :template_name attr_writer :pipeline_status alias_attribute :title, :name @@ -159,7 +164,7 @@ class Project < ActiveRecord::Base has_many :todos has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - has_one :import_data, class_name: 'ProjectImportData' + has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :project_feature has_one :statistics, class_name: 'ProjectStatistics' @@ -188,6 +193,7 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature + accepts_nested_attributes_for :import_data delegate :name, to: :owner, allow_nil: true, prefix: true delegate :count, to: :forks, prefix: true @@ -375,7 +381,7 @@ class Project < ActiveRecord::Base begin Projects::HousekeepingService.new(project).execute rescue Projects::HousekeepingService::LeaseTaken => e - Rails.logger.info("Could not perform housekeeping for project #{project.path_with_namespace} (#{project.id}): #{e}") + Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}") end end end @@ -476,12 +482,12 @@ class Project < ActiveRecord::Base end def repository - @repository ||= Repository.new(path_with_namespace, self) + @repository ||= Repository.new(full_path, self, disk_path: disk_path) end def container_registry_url if Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host_port}/#{path_with_namespace.downcase}" + "#{Gitlab.config.registry.host_port}/#{full_path.downcase}" end end @@ -520,16 +526,16 @@ class Project < ActiveRecord::Base job_id = if forked? RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, - forked_from_project.path_with_namespace, + forked_from_project.full_path, self.namespace.full_path) else RepositoryImportWorker.perform_async(self.id) end if job_id - Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}" else - Rails.logger.error "Import job failed to start for #{path_with_namespace}" + Rails.logger.error "Import job failed to start for #{full_path}" end end @@ -584,8 +590,6 @@ class Project < ActiveRecord::Base project_import_data.credentials ||= {} project_import_data.credentials = project_import_data.credentials.merge(credentials) end - - project_import_data.save end def import? @@ -690,7 +694,7 @@ class Project < ActiveRecord::Base # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) if full || cross_namespace_reference?(from) - path_with_namespace + full_path elsif cross_project_reference?(from) path end @@ -714,7 +718,7 @@ class Project < ActiveRecord::Base author.ensure_incoming_email_token! Gitlab::IncomingEmail.reply_address( - "#{path_with_namespace}+#{author.incoming_email_token}") + "#{full_path}+#{author.incoming_email_token}") end def build_commit_note(commit) @@ -937,11 +941,11 @@ class Project < ActiveRecord::Base end def repo - repository.raw + repository.rugged end def url_to_repo - gitlab_shell.url_to_repo(path_with_namespace) + gitlab_shell.url_to_repo(full_path) end def repo_exists? @@ -974,56 +978,6 @@ class Project < ActiveRecord::Base !group end - def rename_repo - path_was = previous_changes['path'].first - old_path_with_namespace = File.join(namespace.full_path, path_was) - new_path_with_namespace = File.join(namespace.full_path, path) - - Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" - - if has_container_registry_tags? - Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present!" - - # we currently doesn't support renaming repository if it contains images in container registry - raise StandardError.new('Project cannot be renamed, because images are present in its container registry') - end - - expire_caches_before_rename(old_path_with_namespace) - - if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) - # If repository moved successfully we need to send update instructions to users. - # However we cannot allow rollback since we moved repository - # So we basically we mute exceptions in next actions - begin - gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") - send_move_instructions(old_path_with_namespace) - expires_full_path_cache - - @old_path_with_namespace = old_path_with_namespace - - SystemHooksService.new.execute_hooks_for(self, :rename) - - @repository = nil - rescue => e - Rails.logger.error "Exception renaming #{old_path_with_namespace} -> #{new_path_with_namespace}: #{e}" - # Returning false does not rollback after_* transaction but gives - # us information about failing some of tasks - false - end - else - Rails.logger.error "Repository could not be renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" - - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise StandardError.new('repository cannot be renamed') - end - - Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" - - Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.full_path) - Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.full_path) - end - # Expires various caches before a project is renamed. def expire_caches_before_rename(old_path) repo = Repository.new(old_path, self) @@ -1048,7 +1002,7 @@ class Project < ActiveRecord::Base git_http_url: http_url_to_repo, namespace: namespace.name, visibility_level: visibility_level, - path_with_namespace: path_with_namespace, + path_with_namespace: full_path, default_branch: default_branch, ci_config_path: ci_config_path } @@ -1109,19 +1063,6 @@ class Project < ActiveRecord::Base merge_requests.where(source_project_id: self.id) end - def create_repository(force: false) - # Forked import is handled asynchronously - return if forked? && !force - - if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) - repository.after_create - true - else - errors.add(:base, 'Failed to create repository via gitlab-shell') - false - end - end - def ensure_repository create_repository(force: true) unless repository_exists? end @@ -1257,7 +1198,7 @@ class Project < ActiveRecord::Base end def pages_path - File.join(Settings.pages.path, path_with_namespace) + File.join(Settings.pages.path, disk_path) end def public_pages_path @@ -1265,9 +1206,21 @@ class Project < ActiveRecord::Base end def remove_private_deploy_keys - deploy_keys.where(public: false).delete_all + exclude_keys_linked_to_other_projects = <<-SQL + NOT EXISTS ( + SELECT 1 + FROM deploy_keys_projects dkp2 + WHERE dkp2.deploy_key_id = deploy_keys_projects.deploy_key_id + AND dkp2.project_id != deploy_keys_projects.project_id + ) + SQL + + deploy_keys.where(public: false) + .where(exclude_keys_linked_to_other_projects) + .delete_all end + # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal? def remove_pages ::Projects::UpdatePagesConfigurationService.new(self).execute @@ -1315,7 +1268,7 @@ class Project < ActiveRecord::Base end def export_path - File.join(Gitlab::ImportExport.storage_path, path_with_namespace) + File.join(Gitlab::ImportExport.storage_path, disk_path) end def export_project_path @@ -1327,16 +1280,12 @@ class Project < ActiveRecord::Base status.zero? end - def ensure_dir_exist - gitlab_shell.add_namespace(repository_storage_path, namespace.full_path) - end - def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: path, public: true }, - { key: 'CI_PROJECT_PATH', value: path_with_namespace, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: path_with_namespace.parameterize, public: true }, + { key: 'CI_PROJECT_PATH', value: full_path, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: full_path.parameterize, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: web_url, public: true } ] @@ -1441,6 +1390,7 @@ class Project < ActiveRecord::Base alias_method :name_with_namespace, :full_name alias_method :human_name, :full_name + # @deprecated cannot remove yet because it has an index with its name in elasticsearch alias_method :path_with_namespace, :full_path private @@ -1495,7 +1445,7 @@ class Project < ActiveRecord::Base def pending_delete_twin return false unless path - Project.pending_delete.find_by_full_path(path_with_namespace) + Project.pending_delete.find_by_full_path(full_path) end ## diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 37730474324..6da6632f4f2 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -1,7 +1,7 @@ require 'carrierwave/orm/activerecord' class ProjectImportData < ActiveRecord::Base - belongs_to :project + belongs_to :project, inverse_of: :import_data attr_encrypted :credentials, key: Gitlab::Application.secrets.db_key_base, marshal: true, diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 2db95b9aaa3..4d23a17a545 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -35,9 +35,9 @@ class FlowdockService < Service data[:after], token: token, repo: project.repository.path_to_repo, - repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", - commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", - diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s" + repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", + commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s", + diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" ) end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2aa19443198..9ee3a533c1e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -104,7 +104,7 @@ class JiraService < IssueTrackerService def close_issue(entity, external_issue) issue = jira_request { client.Issue.find(external_issue.iid) } - return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present? + return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? commit_id = if entity.is_a?(Commit) entity.id @@ -118,7 +118,7 @@ class JiraService < IssueTrackerService # may or may not be allowed. Refresh the issue after transition and check # if it is closed, so we don't have one comment for every commit. issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) - add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution + add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) end def create_cross_reference_note(mentioned, noteable, author) @@ -140,7 +140,7 @@ class JiraService < IssueTrackerService url: resource_url(user_path(author)) }, project: { - name: project.path_with_namespace, + name: project.full_path, url: resource_url(namespace_project_path(project.namespace, project)) # rubocop:disable Cop/ProjectPathHelper }, entity: { @@ -216,6 +216,10 @@ class JiraService < IssueTrackerService end end + def has_resolution?(issue) + issue.respond_to?(:resolution) && issue.resolution.present? + end + def comment_exists?(issue, message) comments = jira_request { issue.comments } diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index dfca0031af8..698fdf7a20c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -1,5 +1,6 @@ class ProjectWiki include Gitlab::ShellAdapter + include Storage::LegacyProjectWiki MARKUPS = { 'Markdown' => :markdown, @@ -26,16 +27,19 @@ class ProjectWiki @project.path + '.wiki' end - def path_with_namespace - @project.path_with_namespace + ".wiki" + def full_path + @project.full_path + '.wiki' end + # @deprecated use full_path when you need it for an URL route or disk_path when you want to point to the filesystem + alias_method :path_with_namespace, :full_path + def web_url Gitlab::Routing.url_helpers.project_wiki_url(@project, :home) end def url_to_repo - gitlab_shell.url_to_repo(path_with_namespace) + gitlab_shell.url_to_repo(full_path) end def ssh_url_to_repo @@ -43,11 +47,11 @@ class ProjectWiki end def http_url_to_repo - "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git" + "#{Gitlab.config.gitlab.url}/#{full_path}.git" end def wiki_base_path - [Gitlab.config.gitlab.relative_url_root, "/", @project.path_with_namespace, "/wikis"].join('') + [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('') end # Returns the Gollum::Wiki object. @@ -109,10 +113,10 @@ class ProjectWiki return false end - def update_page(page, content, format = :markdown, message = nil) + def update_page(page, content:, title: nil, format: :markdown, message: nil) commit = commit_details(:updated, message, page.title) - wiki.update_page(page, page.name, format.to_sym, content, commit) + wiki.update_page(page, title || page.name, format.to_sym, content, commit) update_project_activity end @@ -134,7 +138,7 @@ class ProjectWiki end def repository - @repository ||= Repository.new(path_with_namespace, @project) + @repository ||= Repository.new(full_path, @project, disk_path: disk_path) end def default_branch @@ -142,7 +146,7 @@ class ProjectWiki end def create_repo! - if init_repo(path_with_namespace) + if init_repo(disk_path) wiki = Gollum::Wiki.new(path_to_repo) else raise CouldNotCreateWikiError @@ -162,15 +166,15 @@ class ProjectWiki web_url: web_url, git_ssh_url: ssh_url_to_repo, git_http_url: http_url_to_repo, - path_with_namespace: path_with_namespace, + path_with_namespace: full_path, default_branch: default_branch } end private - def init_repo(path_with_namespace) - gitlab_shell.add_repository(project.repository_storage_path, path_with_namespace) + def init_repo(disk_path) + gitlab_shell.add_repository(project.repository_storage_path, disk_path) end def commit_details(action, message = nil, title = nil) @@ -184,7 +188,7 @@ class ProjectWiki end def path_to_repo - @path_to_repo ||= File.join(project.repository_storage_path, "#{path_with_namespace}.git") + @path_to_repo ||= File.join(project.repository_storage_path, "#{disk_path}.git") end def update_project_activity diff --git a/app/models/repository.rb b/app/models/repository.rb index 50b7a477904..f86a0869b01 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -4,7 +4,7 @@ class Repository include Gitlab::ShellAdapter include RepositoryMirroring - attr_accessor :path_with_namespace, :project + attr_accessor :full_path, :disk_path, :project delegate :ref_name_for_sha, to: :raw_repository @@ -52,13 +52,14 @@ class Repository end end - def initialize(path_with_namespace, project) - @path_with_namespace = path_with_namespace + def initialize(full_path, project, disk_path: nil) + @full_path = full_path + @disk_path = disk_path || full_path @project = project end def raw_repository - return nil unless path_with_namespace + return nil unless full_path @raw_repository ||= initialize_raw_repository end @@ -66,7 +67,7 @@ class Repository # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( - File.join(repository_storage_path, path_with_namespace + ".git") + File.join(repository_storage_path, disk_path + '.git') ) end @@ -132,12 +133,13 @@ class Repository ref ||= root_ref args = %W( - #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} + log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query} --regexp-ignore-case ) args = args.concat(%W(-- #{path})) if path.present? - git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines + git_log_results = run_git(args).first.lines + git_log_results.map { |c| commit(c.chomp) }.compact end @@ -469,7 +471,7 @@ class Repository # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/314 def exists? - return false unless path_with_namespace + return false unless full_path Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| if enabled @@ -612,17 +614,26 @@ class Repository end def last_commit_for_path(sha, path) - sha = last_commit_id_for_path(sha, path) - commit(sha) + raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled| + if is_enabled + last_commit_for_path_by_gitaly(sha, path) + else + last_commit_for_path_by_rugged(sha, path) + end + end end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/383 def last_commit_id_for_path(sha, path) key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" cache.fetch(key) do - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - Gitlab::Popen.popen(args, path_to_repo).first.strip + raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled| + if is_enabled + last_commit_for_path_by_gitaly(sha, path).id + else + last_commit_id_for_path_by_shelling_out(sha, path) + end + end end end @@ -677,8 +688,8 @@ class Repository end def refs_contains_sha(ref_type, sha) - args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) - names = Gitlab::Popen.popen(args, path_to_repo).first + args = %W(#{ref_type} --contains #{sha}) + names = run_git(args).first if names.respond_to?(:split) names = names.split("\n").map(&:strip) @@ -943,7 +954,7 @@ class Repository if is_enabled raw_repository.is_ancestor?(ancestor_id, descendant_id) else - merge_base_commit(ancestor_id, descendant_id) == ancestor_id + rugged_is_ancestor?(ancestor_id, descendant_id) end end end @@ -956,15 +967,17 @@ class Repository return [] if empty_repo? || query.blank? offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) - Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) + args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) + + run_git(args).first.scrub.split(/^--$/) end def search_files_by_name(query, ref) return [] if empty_repo? || query.blank? - args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) - Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) + args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) + + run_git(args).first.lines.map(&:strip) end def with_repo_branch_commit(start_repository, start_branch_name) @@ -1005,12 +1018,12 @@ class Repository end def fetch_remote(remote, forced: false, no_tags: false) - gitlab_shell.fetch_remote(repository_storage_path, path_with_namespace, remote, forced: forced, no_tags: no_tags) + gitlab_shell.fetch_remote(repository_storage_path, disk_path, remote, forced: forced, no_tags: no_tags) end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - Gitlab::Popen.popen(args, path_to_repo) + args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) + run_git(args) end def create_ref(ref, ref_path) @@ -1091,6 +1104,12 @@ class Repository private + def run_git(args) + circuit_breaker.perform do + Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo) + end + end + def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1100,11 +1119,14 @@ class Repository end def refs_directory_exists? - File.exist?(File.join(path_to_repo, 'refs')) + circuit_breaker.perform do + File.exist?(File.join(path_to_repo, 'refs')) + end end def cache - @cache ||= RepositoryCache.new(path_with_namespace, @project.id) + # TODO: should we use UUIDs here? We could move repositories without clearing this cache + @cache ||= RepositoryCache.new(full_path, @project.id) end def tags_sorted_by_committed_date @@ -1127,7 +1149,7 @@ class Repository end def repository_event(event, tags = {}) - Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) + Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags)) end def create_commit(params = {}) @@ -1136,11 +1158,30 @@ class Repository Rugged::Commit.create(rugged, params) end + def last_commit_for_path_by_gitaly(sha, path) + c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) + commit(c) + end + + def last_commit_for_path_by_rugged(sha, path) + sha = last_commit_id_for_path_by_shelling_out(sha, path) + commit(sha) + end + + def last_commit_id_for_path_by_shelling_out(sha, path) + args = %W(rev-list --max-count=1 #{sha} -- #{path}) + run_git(args).first.strip + end + def repository_storage_path @project.repository_storage_path end def initialize_raw_repository - Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git') + Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git') + end + + def circuit_breaker + @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage) end end diff --git a/app/models/user.rb b/app/models/user.rb index 6e66c587a1f..5148886eed7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,6 +47,11 @@ class User < ActiveRecord::Base devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable + # devise overrides #inspect, so we manually use the Referable one + def inspect + referable_inspect + end + # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour def update_tracked_fields!(request) @@ -627,7 +632,11 @@ class User < ActiveRecord::Base end def projects_limit_left - projects_limit - personal_projects.count + projects_limit - personal_projects_count + end + + def personal_projects_count + @personal_projects_count ||= personal_projects.count end def projects_limit_percent @@ -641,16 +650,14 @@ class User < ActiveRecord::Base events = events.where(project_id: project_ids) if project_ids # Use the latest event that has not been pushed or merged recently - events.recent.find do |event| - project = Project.find_by_id(event.project_id) - next unless project - - if project.repository.branch_exists?(event.branch_name) - merge_requests = MergeRequest.where("created_at >= ?", event.created_at) - .where(source_project_id: project.id, - source_branch: event.branch_name) - merge_requests.empty? - end + events.includes(:project).recent.find do |event| + next unless event.project.repository.branch_exists?(event.branch_name) + + merge_requests = MergeRequest.where("created_at >= ?", event.created_at) + .where(source_project_id: event.project.id, + source_branch: event.branch_name) + + merge_requests.empty? end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 148998bc9be..5c7c2204374 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -180,31 +180,50 @@ class WikiPage # # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. - def create(attr = {}) - @attributes.merge!(attr) + def create(attrs = {}) + @attributes.merge!(attrs) - save :create_page, title, content, format, message + save(page_details: title) do + wiki.create_page(title, content, format, message) + end end # Updates an existing Wiki Page, creating a new version. # - # new_content - The raw markup content to replace the existing. - # format - Optional symbol representing the content format. - # See ProjectWiki::MARKUPS Hash for available formats. - # message - Optional commit message to set on the new version. - # last_commit_sha - Optional last commit sha to validate the page unchanged. + # attrs - Hash of attributes to be updated on the page. + # :content - The raw markup content to replace the existing. + # :format - Optional symbol representing the content format. + # See ProjectWiki::MARKUPS Hash for available formats. + # :message - Optional commit message to set on the new version. + # :last_commit_sha - Optional last commit sha to validate the page unchanged. + # :title - The Title to replace existing title # # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. - def update(new_content, format: :markdown, message: nil, last_commit_sha: nil) - @attributes[:content] = new_content - @attributes[:format] = format - + def update(attrs = {}) + last_commit_sha = attrs.delete(:last_commit_sha) if last_commit_sha && last_commit_sha != self.last_commit_sha raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.") end - save :update_page, @page, content, format, message + attrs.slice!(:content, :format, :message, :title) + @attributes.merge!(attrs) + page_details = + if title.present? && @page.title != title + title + else + @page.url_path + end + + save(page_details: page_details) do + wiki.update_page( + @page, + content: content, + format: format, + message: attrs[:message], + title: title + ) + end end # Destroys the Wiki Page. @@ -236,30 +255,19 @@ class WikiPage attributes[:format] = @page.format end - def save(method, *args) - saved = false + def save(page_details:) + return unless valid? - project_wiki = wiki - if valid? && project_wiki.send(method, *args) - - page_details = if method == :update_page - # Use url_path instead of path to omit format extension - @page.url_path - else - title - end - - page_title, page_dir = project_wiki.page_title_and_dir(page_details) - gollum_wiki = project_wiki.wiki - @page = gollum_wiki.paged(page_title, page_dir) + unless yield + errors.add(:base, wiki.error_message) + return false + end - set_attributes + page_title, page_dir = wiki.page_title_and_dir(page_details) + gollum_wiki = wiki.wiki + @page = gollum_wiki.paged(page_title, page_dir) - @persisted = true - saved = true - else - errors.add(:base, project_wiki.error_message) if project_wiki.error_message - end - saved + set_attributes + @persisted = errors.blank? end end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 1c91425f589..1be7bbe9953 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -44,7 +44,7 @@ class GlobalPolicy < BasePolicy prevent :log_in end - rule { admin | ~restricted_public_level }.policy do + rule { ~(anonymous & restricted_public_level) }.policy do enable :read_users_list end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 5e151b0f044..7dae5880931 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -103,6 +103,8 @@ module Auth build_can_pull?(requested_project) || user_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) + when '*' + user_can_admin?(requested_project) else false end @@ -120,6 +122,11 @@ module Auth (requested_project == project || can?(current_user, :build_read_container_image, requested_project)) end + def user_can_admin?(requested_project) + has_authentication_ability?(:admin_container_image) && + can?(current_user, :admin_container_image, requested_project) + end + def user_can_pull?(requested_project) has_authentication_ability?(:read_container_image) && can?(current_user, :read_container_image, requested_project) diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index b951e8d0c9f..fc87bd6a659 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -30,6 +30,7 @@ module Ci # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. build.runner_id = runner.id build.run! + register_success(build) return Result.new(build, true) rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError @@ -46,6 +47,7 @@ module Ci end end + register_failure Result.new(nil, valid) end @@ -81,5 +83,27 @@ module Ci def shared_runner_build_limits_feature_enabled? ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true' end + + def register_failure + failed_attempt_counter.increase + attempt_counter.increase + end + + def register_success(job) + job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) + attempt_counter.increase + end + + def failed_attempt_counter + @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") + end + + def attempt_counter + @attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_total, "Counts the times a runner tries to register a job") + end + + def job_queue_duration_seconds + @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time') + end end end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index 5c9e2a16c71..ff11bd59d29 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -11,7 +11,7 @@ class DeleteMergedBranchesService < BaseService # Prevent deletion of branches relevant to open merge requests branches -= merge_request_branch_names # Prevent deletion of protected branches - branches -= project.protected_branches.pluck(:name) + branches = branches.reject { |branch| project.protected_for?(branch) } branches.each do |branch| DeleteBranchService.new(project, current_user).execute(branch) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 32925e9c1f2..545ca0742e4 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -60,7 +60,7 @@ class GitOperationService start_branch_name = nil if start_repository.empty_repo? if start_branch_name && !start_repository.branch_exists?(start_branch_name) - raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.path_with_namespace}" + raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}" end update_branch_with_hooks(branch_name) do diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 0e25c555136..b84a6fd2b7d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -2,11 +2,8 @@ class IssuableBaseService < BaseService private def create_milestone_note(issuable) - milestone = issuable.milestone - return if milestone && milestone.is_group_milestone? - SystemNoteService.change_milestone( - issuable, issuable.project, current_user, milestone) + issuable, issuable.project, current_user, issuable.milestone) end def create_labels_note(issuable, old_labels) @@ -287,7 +284,7 @@ class IssuableBaseService < BaseService todo_service.mark_todo(issuable, current_user) when 'done' todo = TodosFinder.new(current_user).execute.find_by(target: issuable) - todo_service.mark_todos_as_done([todo], current_user) if todo + todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo end end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index eb04048b748..7d539fa49e6 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -12,7 +12,6 @@ module MergeRequests merge_request.source_project = source_project merge_request.source_branch = params[:source_branch] merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) - merge_request.head_pipeline = head_pipeline_for(merge_request) create(merge_request) end @@ -28,10 +27,16 @@ module MergeRequests event_service.open_mr(issuable, current_user) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) + update_merge_requests_head_pipeline(issuable) end private + def update_merge_requests_head_pipeline(merge_request) + pipeline = head_pipeline_for(merge_request) + merge_request.update(head_pipeline_id: pipeline.id) if pipeline + end + def head_pipeline_for(merge_request) return unless merge_request.source_project diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 9ac561e4bd2..21c9c314a2a 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -1,331 +1,288 @@ # # Used by NotificationService to determine who should receive notification # -class NotificationRecipientService - attr_reader :project - - def initialize(project) - @project = project +module NotificationRecipientService + def self.notifiable_users(users, *args) + users.compact.map { |u| NotificationRecipient.new(u, *args) }.select(&:notifiable?).map(&:user) end - def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true) - custom_action = build_custom_key(action, target) - - recipients = participants(target, current_user) - recipients = add_project_watchers(recipients) - recipients = add_custom_notifications(recipients, custom_action) - recipients = reject_mention_users(recipients) - - # Re-assign is considered as a mention of the new assignee so we add the - # new assignee to the list of recipients after we rejected users with - # the "on mention" notification level - case custom_action - when :reassign_merge_request - recipients << previous_assignee if previous_assignee - recipients << target.assignee - when :reassign_issue - previous_assignees = Array(previous_assignee) - recipients.concat(previous_assignees) - recipients.concat(target.assignees) - end - - recipients = reject_muted_users(recipients) - recipients = add_subscribed_users(recipients, target) - - if [:new_issue, :new_merge_request].include?(custom_action) - recipients = add_labels_subscribers(recipients, target) - end - - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) + def self.notifiable?(user, *args) + NotificationRecipient.new(user, *args).notifiable? + end - recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? + def self.build_recipients(*a) + Builder::Default.new(*a).recipient_users + end - recipients.uniq + def self.build_new_note_recipients(*a) + Builder::NewNote.new(*a).recipient_users end - def build_pipeline_recipients(target, current_user, action:) - return [] unless current_user + module Builder + class Base + def initialize(*) + raise 'abstract' + end - custom_action = - case action.to_s - when 'failed' - :failed_pipeline - when 'success' - :success_pipeline + def build! + raise 'abstract' end - notification_setting = notification_setting_for_user_project(current_user, target.project) + def filter! + recipients.select!(&:notifiable?) + end - return [] if notification_setting.mention? || notification_setting.disabled? + def acting_user + current_user + end - return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) + def target + raise 'abstract' + end - return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + # rubocop:disable Rails/Delegate + def project + target.project + end - reject_users_without_access([current_user], target) - end + def recipients + @recipients ||= [] + end - def build_relabeled_recipients(target, current_user, labels:) - recipients = add_labels_subscribers([], target, labels: labels) - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) unless current_user.notified_of_own_activity? - recipients.uniq - end + def <<(pair) + users, type = pair - def build_new_note_recipients(note) - target = note.noteable + if users.is_a?(ActiveRecord::Relation) + users = users.includes(:notification_settings) + end - ability, subject = if note.for_personal_snippet? - [:read_personal_snippet, note.noteable] - else - [:read_project, note.project] - end + users = Array(users) + users.compact! + recipients.concat(users.map { |u| make_recipient(u, type) }) + end - mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } + def user_scope + User.includes(:notification_settings) + end - # Add all users participating in the thread (author, assignee, comment authors) - recipients = participants(target, note.author) || mentioned_users + def make_recipient(user, type) + NotificationRecipient.new( + user, type, + project: project, + custom_action: custom_action, + target: target, + acting_user: acting_user + ) + end - unless note.for_personal_snippet? - # Merge project watchers - recipients = add_project_watchers(recipients) + def recipient_users + @recipient_users ||= + begin + build! + filter! + users = recipients.map(&:user) + users.uniq! + users.freeze + end + end - # Merge project with custom notification - recipients = add_custom_notifications(recipients, :new_note) - end + def custom_action + nil + end - # Reject users with Mention notification level, except those mentioned in _this_ note. - recipients = reject_mention_users(recipients - mentioned_users) - recipients = recipients + mentioned_users + protected - recipients = reject_muted_users(recipients) + def add_participants(user) + return unless target.respond_to?(:participants) - recipients = add_subscribed_users(recipients, note.noteable) - recipients = reject_unsubscribed_users(recipients, note.noteable) - recipients = reject_users_without_access(recipients, note.noteable) + self << [target.participants(user), :watch] + end - recipients.delete(note.author) unless note.author.notified_of_own_activity? - recipients.uniq - end + # Get project/group users with CUSTOM notification level + def add_custom_notifications + user_ids = [] - # Remove users with disabled notifications from array - # Also remove duplications and nil recipients - def reject_muted_users(users) - reject_users(users, :disabled) - end + # Users with a notification setting on group or project + user_ids += user_ids_notifiable_on(project, :custom) + user_ids += user_ids_notifiable_on(project.group, :custom) - protected + # Users with global level custom + user_ids_with_project_level_global = user_ids_notifiable_on(project, :global) + user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global) - # Ensure that if we modify this array, we aren't modifying the memoised - # participants on the target. - def participants(target, user) - return unless target.respond_to?(:participants) + global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) + user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action) - target.participants(user).dup - end + self << [user_scope.where(id: user_ids), :watch] + end - # Get project/group users with CUSTOM notification level - def add_custom_notifications(recipients, action) - user_ids = [] + def add_project_watchers + self << [project_watchers, :watch] + end - # Users with a notification setting on group or project - user_ids += user_ids_notifiable_on(project, :custom, action) - user_ids += user_ids_notifiable_on(project.group, :custom, action) + # Get project users with WATCH notification level + def project_watchers + project_members_ids = user_ids_notifiable_on(project) - # Users with global level custom - user_ids_with_project_level_global = user_ids_notifiable_on(project, :global) - user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global) + user_ids_with_project_global = user_ids_notifiable_on(project, :global) + user_ids_with_group_global = user_ids_notifiable_on(project.group, :global) - global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) - user_ids += user_ids_with_global_level_custom(global_users_ids, action) + user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq) - recipients.concat(User.find(user_ids)) - end + user_ids_with_project_setting = select_project_members_ids(user_ids_with_project_global, user_ids) + user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) - def add_project_watchers(recipients) - recipients.concat(project_watchers).compact - end + user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq) + end - # Get project users with WATCH notification level - def project_watchers - project_members_ids = user_ids_notifiable_on(project) + def add_subscribed_users + return unless target.respond_to? :subscribers - user_ids_with_project_global = user_ids_notifiable_on(project, :global) - user_ids_with_group_global = user_ids_notifiable_on(project.group, :global) + self << [target.subscribers(project), :subscription] + end - user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq) + def user_ids_notifiable_on(resource, notification_level = nil) + return [] unless resource - user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids) - user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) + scope = resource.notification_settings - User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a - end + if notification_level + scope = scope.where(level: NotificationSetting.levels[notification_level]) + end - # Remove users with notification level 'Mentioned' - def reject_mention_users(users) - reject_users(users, :mention) - end + scope.pluck(:user_id) + end - def add_subscribed_users(recipients, target) - return recipients unless target.respond_to? :subscribers + # Build a list of user_ids based on project notification settings + def select_project_members_ids(global_setting, user_ids_global_level_watch) + user_ids = user_ids_notifiable_on(project, :watch) - recipients + target.subscribers(project) - end + # If project setting is global, add to watch list if global setting is watch + user_ids + (global_setting & user_ids_global_level_watch) + end - def user_ids_notifiable_on(resource, notification_level = nil, action = nil) - return [] unless resource + # Build a list of user_ids based on group notification settings + def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch) + uids = user_ids_notifiable_on(group, :watch) - if notification_level - settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) - settings = settings.select { |setting| setting.event_enabled?(action) } if action.present? - settings.map(&:user_id) - else - resource.notification_settings.pluck(:user_id) - end - end + # Group setting is global, add to user_ids list if global setting is watch + uids + (global_setting & user_ids_global_level_watch) - project_members + end - # Build a list of user_ids based on project notification settings - def select_project_members_ids(project, global_setting, user_ids_global_level_watch) - user_ids = user_ids_notifiable_on(project, :watch) + def user_ids_with_global_level_watch(ids) + settings_with_global_level_of(:watch, ids).pluck(:user_id) + end - # If project setting is global, add to watch list if global setting is watch - global_setting.each do |user_id| - if user_ids_global_level_watch.include?(user_id) - user_ids << user_id + def user_ids_with_global_level_custom(ids, action) + settings_with_global_level_of(:custom, ids).pluck(:user_id) end - end - user_ids - end + def settings_with_global_level_of(level, ids) + NotificationSetting.where( + user_id: ids, + source_type: nil, + level: NotificationSetting.levels[level] + ) + end - # Build a list of user_ids based on group notification settings - def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch) - uids = user_ids_notifiable_on(group, :watch) + def add_labels_subscribers(labels: nil) + return unless target.respond_to? :labels - # Group setting is watch, add to user_ids list if user is not project member - user_ids = [] - uids.each do |user_id| - if project_members.exclude?(user_id) - user_ids << user_id + (labels || target.labels).each do |label| + self << [label.subscribers(project), :subscription] + end end end - # Group setting is global, add to user_ids list if global setting is watch - global_setting.each do |user_id| - if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id) - user_ids << user_id + class Default < Base + attr_reader :target + attr_reader :current_user + attr_reader :action + attr_reader :previous_assignee + attr_reader :skip_current_user + def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true) + @target = target + @current_user = current_user + @action = action + @previous_assignee = previous_assignee + @skip_current_user = skip_current_user end - end - - user_ids - end - - def user_ids_with_global_level_watch(ids) - settings_with_global_level_of(:watch, ids).pluck(:user_id) - end - - def user_ids_with_global_level_custom(ids, action) - settings = settings_with_global_level_of(:custom, ids) - settings = settings.select { |setting| setting.event_enabled?(action) } - settings.map(&:user_id) - end - def settings_with_global_level_of(level, ids) - NotificationSetting.where( - user_id: ids, - source_type: nil, - level: NotificationSetting.levels[level] - ) - end + def build! + add_participants(current_user) + add_project_watchers + add_custom_notifications + + # Re-assign is considered as a mention of the new assignee + case custom_action + when :reassign_merge_request + self << [previous_assignee, :mention] + self << [target.assignee, :mention] + when :reassign_issue + previous_assignees = Array(previous_assignee) + self << [previous_assignees, :mention] + self << [target.assignees, :mention] + end + + add_subscribed_users + + if [:new_issue, :new_merge_request].include?(custom_action) + add_labels_subscribers + end + end - # Reject users which has certain notification level - # - # Example: - # reject_users(users, :watch, project) - # - def reject_users(users, level) - level = level.to_s + def acting_user + current_user if skip_current_user + end - unless NotificationSetting.levels.keys.include?(level) - raise 'Invalid notification level' + # Build event key to search on custom notification level + # Check NotificationSetting::EMAIL_EVENTS + def custom_action + @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym + end end - users = users.to_a.compact.uniq - users = users.select { |u| u.can?(:receive_notifications) } - - users.reject do |user| - global_notification_setting = user.global_notification_setting - - next global_notification_setting.level == level unless project - - setting = user.notification_settings_for(project) - - if project.group && (setting.nil? || setting.global?) - setting = user.notification_settings_for(project.group) + class NewNote < Base + attr_reader :note + def initialize(note) + @note = note end - # reject users who globally set mention notification and has no setting per project/group - next global_notification_setting.level == level unless setting - - # reject users who set mention notification in project - next true if setting.level == level - - # reject users who have mention level in project and disabled in global settings - setting.global? && global_notification_setting.level == level - end - end + def target + note.noteable + end - def reject_unsubscribed_users(recipients, target) - return recipients unless target.respond_to? :subscriptions + # NOTE: may be nil, in the case of a PersonalSnippet + # + # (this is okay because NotificationRecipient is written + # to handle nil projects) + def project + note.project + end - recipients.reject do |user| - subscription = target.subscriptions.find_by_user_id(user.id) - subscription && !subscription.subscribed - end - end + def build! + # Add all users participating in the thread (author, assignee, comment authors) + add_participants(note.author) + self << [note.mentioned_users, :mention] - def reject_users_without_access(recipients, target) - ability = case target - when Issuable - :"read_#{target.to_ability_name}" - when Ci::Pipeline - :read_build # We have build trace in pipeline emails - end + unless note.for_personal_snippet? + # Merge project watchers + add_project_watchers - return recipients unless ability + # Merge project with custom notification + add_custom_notifications + end - recipients.select do |user| - user.can?(ability, target) - end - end + add_subscribed_users + end - def add_labels_subscribers(recipients, target, labels: nil) - return recipients unless target.respond_to? :labels + def custom_action + :new_note + end - (labels || target.labels).each do |label| - recipients += label.subscribers(project) + def acting_user + note.author + end end - - recipients - end - - # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS - def build_custom_key(action, object) - "#{action}_#{object.class.model_name.name.underscore}".to_sym - end - - def notification_setting_for_user_project(user, project) - project_setting = user.notification_settings_for(project) - - return project_setting unless project_setting.global? - - group_setting = user.notification_settings_for(project.group) - - return group_setting unless group_setting.global? - - user.global_notification_setting end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index b94921d2a08..df04b1a4fe3 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -42,7 +42,7 @@ class NotificationService # * users with custom level checked with "new issue" # def new_issue(issue, current_user) - new_resource_email(issue, issue.project, :new_issue_email) + new_resource_email(issue, :new_issue_email) end # When issue text is updated, we should send an email to: @@ -52,7 +52,6 @@ class NotificationService def new_mentions_in_issue(issue, new_mentioned_users, current_user) new_mentions_in_resource_email( issue, - issue.project, new_mentioned_users, current_user, :new_mention_in_issue_email @@ -67,7 +66,7 @@ class NotificationService # * users with custom level checked with "close issue" # def close_issue(issue, current_user) - close_resource_email(issue, issue.project, current_user, :closed_issue_email) + close_resource_email(issue, current_user, :closed_issue_email) end # When we reassign an issue we should send an email to: @@ -77,7 +76,7 @@ class NotificationService # * users with custom level checked with "reassign issue" # def reassigned_issue(issue, current_user, previous_assignees = []) - recipients = NotificationRecipientService.new(issue.project).build_recipients( + recipients = NotificationRecipientService.build_recipients( issue, current_user, action: "reassign", @@ -102,7 +101,7 @@ class NotificationService # * watchers of the issue's labels # def relabeled_issue(issue, added_labels, current_user) - relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email) + relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email) end # When create a merge request we should send an email to: @@ -113,7 +112,7 @@ class NotificationService # * users with custom level checked with "new merge request" # def new_merge_request(merge_request, current_user) - new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email) + new_resource_email(merge_request, :new_merge_request_email) end # When merge request text is updated, we should send an email to: @@ -123,7 +122,6 @@ class NotificationService def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user) new_mentions_in_resource_email( merge_request, - merge_request.target_project, new_mentioned_users, current_user, :new_mention_in_merge_request_email @@ -137,7 +135,7 @@ class NotificationService # * users with custom level checked with "reassign merge request" # def reassigned_merge_request(merge_request, current_user) - reassign_resource_email(merge_request, merge_request.target_project, current_user, :reassigned_merge_request_email) + reassign_resource_email(merge_request, current_user, :reassigned_merge_request_email) end # When we add labels to a merge request we should send an email to: @@ -145,21 +143,20 @@ class NotificationService # * watchers of the mr's labels # def relabeled_merge_request(merge_request, added_labels, current_user) - relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email) + relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email) end def close_mr(merge_request, current_user) - close_resource_email(merge_request, merge_request.target_project, current_user, :closed_merge_request_email) + close_resource_email(merge_request, current_user, :closed_merge_request_email) end def reopen_issue(issue, current_user) - reopen_resource_email(issue, issue.project, current_user, :issue_status_changed_email, 'reopened') + reopen_resource_email(issue, current_user, :issue_status_changed_email, 'reopened') end def merge_mr(merge_request, current_user) close_resource_email( merge_request, - merge_request.target_project, current_user, :merged_merge_request_email, skip_current_user: !merge_request.merge_when_pipeline_succeeds? @@ -169,7 +166,6 @@ class NotificationService def reopen_mr(merge_request, current_user) reopen_resource_email( merge_request, - merge_request.target_project, current_user, :merge_request_status_email, 'reopened' @@ -177,7 +173,7 @@ class NotificationService end def resolve_all_discussions(merge_request, current_user) - recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients( + recipients = NotificationRecipientService.build_recipients( merge_request, current_user, action: "resolve_all_discussions") @@ -202,7 +198,7 @@ class NotificationService notify_method = "note_#{note.to_ability_name}_email".to_sym - recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note) + recipients = NotificationRecipientService.build_new_note_recipients(note) recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later end @@ -270,8 +266,7 @@ class NotificationService end def project_was_moved(project, old_path_with_namespace) - recipients = project.team.members - recipients = NotificationRecipientService.new(project).reject_muted_users(recipients) + recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project) recipients.each do |recipient| mailer.project_was_moved_email( @@ -283,7 +278,7 @@ class NotificationService end def issue_moved(issue, new_issue, current_user) - recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user, action: 'moved') + recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved') recipients.map do |recipient| email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) @@ -305,10 +300,10 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients( - pipeline, - pipeline.user, - action: pipeline.status + recipients ||= NotificationRecipientService.notifiable_users( + [pipeline.user], :watch, + custom_action: :"#{pipeline.status}_pipeline", + target: pipeline ).map(&:notification_email) if recipients.any? @@ -318,16 +313,16 @@ class NotificationService protected - def new_resource_email(target, project, method) - recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new") + def new_resource_email(target, method) + recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new") recipients.each do |recipient| mailer.send(method, recipient.id, target.id).deliver_later end end - def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method) - recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new") + def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method) + recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new") recipients = recipients & new_mentioned_users recipients.each do |recipient| @@ -335,10 +330,10 @@ class NotificationService end end - def close_resource_email(target, project, current_user, method, skip_current_user: true) + def close_resource_email(target, current_user, method, skip_current_user: true) action = method == :merged_merge_request_email ? "merge" : "close" - recipients = NotificationRecipientService.new(project).build_recipients( + recipients = NotificationRecipientService.build_recipients( target, current_user, action: action, @@ -350,11 +345,11 @@ class NotificationService end end - def reassign_resource_email(target, project, current_user, method) + def reassign_resource_email(target, current_user, method) previous_assignee_id = previous_record(target, 'assignee_id') previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - recipients = NotificationRecipientService.new(project).build_recipients( + recipients = NotificationRecipientService.build_recipients( target, current_user, action: "reassign", @@ -372,8 +367,14 @@ class NotificationService end end - def relabeled_resource_email(target, project, labels, current_user, method) - recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels) + def relabeled_resource_email(target, labels, current_user, method) + recipients = labels.flat_map { |l| l.subscribers(target.project) } + recipients = NotificationRecipientService.notifiable_users( + recipients, :subscription, + target: target, + acting_user: current_user + ) + label_names = labels.map(&:name) recipients.each do |recipient| @@ -381,8 +382,8 @@ class NotificationService end end - def reopen_resource_email(target, project, current_user, method, status) - recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen") + def reopen_resource_email(target, current_user, method, status) + recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") recipients.each do |recipient| mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index fc85f398935..724a77c873a 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -5,7 +5,15 @@ module Projects end def milestones - @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title]) + finder_params = { + project_ids: [@project.id], + state: :active, + order: { due_date: :asc, title: :asc } + } + + finder_params[:group_ids] = [@project.group.id] if @project.group + + MilestonesFinder.new(finder_params).execute.select([:iid, :title]) end def merge_requests diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb new file mode 100644 index 00000000000..87d9ed7a0e6 --- /dev/null +++ b/app/services/projects/create_from_template_service.rb @@ -0,0 +1,15 @@ +module Projects + class CreateFromTemplateService < BaseService + def initialize(user, params) + @current_user, @params = user, params.dup + end + + def execute + params[:file] = Gitlab::ProjectTemplate.find(params[:template_name]).file + + GitlabProjectsImportService.new(@current_user, @params).execute + ensure + params[:file]&.close + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index e874a2d8789..48578b6d9e5 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -5,6 +5,10 @@ module Projects end def execute + if @params[:template_name]&.present? + return ::Projects::CreateFromTemplateService.new(current_user, params).execute + end + forked_from_project_id = params.delete(:forked_from_project_id) import_data = params.delete(:import_data) @skip_wiki = params.delete(:skip_wiki) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index f6e8b6655f2..11ad4838471 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -9,7 +9,7 @@ module Projects def async_execute project.update_attribute(:pending_delete, true) job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) - Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}") + Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.full_path} with job ID #{job_id}") end def execute @@ -40,7 +40,7 @@ module Projects private def repo_path - project.path_with_namespace + project.disk_path end def wiki_path @@ -127,7 +127,7 @@ module Projects def flush_caches(project) project.repository.before_delete - Repository.new(wiki_path, project).before_delete + Repository.new(wiki_path, project, disk_path: repo_path).before_delete end end end diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb new file mode 100644 index 00000000000..4ca6414b73b --- /dev/null +++ b/app/services/projects/gitlab_projects_import_service.rb @@ -0,0 +1,36 @@ +# This service is an adapter used to for the GitLab Import feature, and +# creating a project from a template. +# The latter will under the hood just import an archive supplied by GitLab. +module Projects + class GitlabProjectsImportService + attr_reader :current_user, :params + + def initialize(user, params) + @current_user, @params = user, params.dup + end + + def execute + FileUtils.mkdir_p(File.dirname(import_upload_path)) + FileUtils.copy_entry(file.path, import_upload_path) + + Gitlab::ImportExport::ProjectCreator.new(params[:namespace_id], + current_user, + import_upload_path, + params[:path]).execute + end + + private + + def import_upload_path + @import_upload_path ||= Gitlab::ImportExport.import_upload_path(filename: tmp_filename) + end + + def tmp_filename + "#{SecureRandom.hex}_#{params[:path]}" + end + + def file + params[:file] + end + end +end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 535da706159..fe4e8ea10bf 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,7 +2,7 @@ module Projects module ImportExport class ExportService < BaseService def execute(_options = {}) - @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) + @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.disk_path, 'work')) save_all end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index eea17e24903..c3bf0031409 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -11,7 +11,7 @@ module Projects success rescue => e - error("Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}") + error("Error importing repository #{project.import_url} into #{project.full_path} - #{e.message}") end private @@ -34,8 +34,12 @@ module Projects def import_repository raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url) + # We should return early for a GitHub import because the new GitHub + # importer fetch the project repositories for us. + return if project.github_import? + begin - if project.github_import? || project.gitea_import? + if project.gitea_import? fetch_repository else clone_repository @@ -51,11 +55,11 @@ module Projects end def clone_repository - gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) + gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url) end def fetch_repository - project.create_repository + project.ensure_repository project.repository.add_remote(project.import_type, project.import_url) project.repository.set_remote_as_mirror(project.import_type) project.repository.fetch_remote(project.import_type, forced: true) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 4bb98e5cb4e..5957f612e84 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,7 +34,7 @@ module Projects private def transfer(project) - @old_path = project.path_with_namespace + @old_path = project.full_path @old_group = project.group @new_path = File.join(@new_namespace.try(:full_path) || '', project.path) @old_namespace = project.namespace @@ -61,11 +61,13 @@ module Projects project.send_move_instructions(@old_path) # Move main repository + # TODO: check storage type and NOOP when not using Legacy unless move_repo_folder(@old_path, @new_path) raise TransferError.new('Cannot move project') end # Move wiki repo also if present + # TODO: check storage type and NOOP when not using Legacy move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") # Move missing group labels to project diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 749a1cc56d8..5038155ca31 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -33,8 +33,10 @@ module Projects success end rescue => e + register_failure error(e.message) ensure + register_attempt build.erase_artifacts! unless build.has_expiring_artifacts? end @@ -168,5 +170,21 @@ module Projects def sha build.sha end + + def register_attempt + pages_deployments_total_counter.increase + end + + def register_failure + pages_deployments_failed_total_counter.increase + end + + def pages_deployments_total_counter + @pages_deployments_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered") + end + + def pages_deployments_failed_total_counter + @pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed") + end end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index c22bf7498bb..c7832c47e1a 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -511,7 +511,12 @@ module QuickActions users = extract_references(params, :user) if users.empty? - users = User.where(username: params.split(' ').map(&:strip)) + users = + if params == 'me' + [current_user] + else + User.where(username: params.split(' ').map(&:strip)) + end end users diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 17857ca62f2..14171bce782 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -1,6 +1,16 @@ class SubmitUsagePingService URL = 'https://version.gitlab.com/usage_data'.freeze + METRICS = %w[leader_issues instance_issues percentage_issues leader_notes instance_notes + percentage_notes leader_milestones instance_milestones percentage_milestones + leader_boards instance_boards percentage_boards leader_merge_requests + instance_merge_requests percentage_merge_requests leader_ci_pipelines + instance_ci_pipelines percentage_ci_pipelines leader_environments instance_environments + percentage_environments leader_deployments instance_deployments percentage_deployments + leader_projects_prometheus_active instance_projects_prometheus_active + percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues + percentage_service_desk_issues].freeze + include Gitlab::CurrentSettings def execute @@ -27,15 +37,7 @@ class SubmitUsagePingService return unless response['conv_index'].present? ConversationalDevelopmentIndex::Metric.create!( - response['conv_index'].slice( - 'leader_issues', 'instance_issues', 'leader_notes', 'instance_notes', - 'leader_milestones', 'instance_milestones', 'leader_boards', 'instance_boards', - 'leader_merge_requests', 'instance_merge_requests', 'leader_ci_pipelines', - 'instance_ci_pipelines', 'leader_environments', 'instance_environments', - 'leader_deployments', 'instance_deployments', 'leader_projects_prometheus_active', - 'instance_projects_prometheus_active', 'leader_service_desk_issues', - 'instance_service_desk_issues' - ) + response['conv_index'].slice(*METRICS) ) end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index bd58a54592f..cbcd4478af6 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -24,7 +24,7 @@ class SystemHooksService key: model.key, id: model.id ) - + if model.user data[:username] = model.user.username end @@ -56,7 +56,7 @@ class SystemHooksService when GroupMember data.merge!(group_member_data(model)) end - + data end @@ -79,7 +79,7 @@ class SystemHooksService { name: model.name, path: model.path, - path_with_namespace: model.path_with_namespace, + path_with_namespace: model.full_path, project_id: model.id, owner_name: owner.name, owner_email: owner.respond_to?(:email) ? owner.email : "", @@ -93,7 +93,7 @@ class SystemHooksService { project_name: project.name, project_path: project.path, - project_path_with_namespace: project.path_with_namespace, + project_path_with_namespace: project.full_path, project_id: project.id, user_username: model.user.username, user_name: model.user.name, diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 2dbee9c246e..1763f64a4e4 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -142,7 +142,8 @@ module SystemNoteService # # Returns the created Note object def change_milestone(noteable, project, author, milestone) - body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}" + format = milestone&.is_group_milestone? ? :name : :iid + body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}" create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone')) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 322c6286365..6ee96d6a0f8 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -170,20 +170,22 @@ class TodoService # When user marks some todos as done def mark_todos_as_done(todos, current_user) - update_todos_state_by_ids(todos.select(&:id), current_user, :done) + update_todos_state(todos, current_user, :done) end def mark_todos_as_done_by_ids(ids, current_user) - update_todos_state_by_ids(ids, current_user, :done) + todos = todos_by_ids(ids, current_user) + mark_todos_as_done(todos, current_user) end # When user marks some todos as pending def mark_todos_as_pending(todos, current_user) - update_todos_state_by_ids(todos.select(&:id), current_user, :pending) + update_todos_state(todos, current_user, :pending) end def mark_todos_as_pending_by_ids(ids, current_user) - update_todos_state_by_ids(ids, current_user, :pending) + todos = todos_by_ids(ids, current_user) + mark_todos_as_pending(todos, current_user) end # When user marks an issue as todo @@ -198,9 +200,11 @@ class TodoService private - def update_todos_state_by_ids(ids, current_user, state) - todos = current_user.todos.where(id: ids) + def todos_by_ids(ids, current_user) + current_user.todos.where(id: Array(ids)) + end + def update_todos_state(todos, current_user, state) # Only update those that are not really on that state todos = todos.where.not(state: state) todos_ids = todos.pluck(:id) diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 27c3ba197ac..2825478926a 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -101,7 +101,7 @@ class WebHookService request_headers: build_headers(hook_name), request_data: request_data, response_headers: format_response_headers(response), - response_body: response.body, + response_body: safe_response_body(response), response_status: response.code, internal_error_message: error_message ) @@ -124,4 +124,10 @@ class WebHookService def format_response_headers(response) response.headers.each_capitalized.to_h end + + def safe_response_body(response) + return '' unless response.body + + response.body.encode('UTF-8', invalid: :replace, undef: :replace, replace: '') + end end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb index c628e6781af..93cbd9a509f 100644 --- a/app/services/wiki_pages/update_service.rb +++ b/app/services/wiki_pages/update_service.rb @@ -1,7 +1,7 @@ module WikiPages class UpdateService < WikiPages::BaseService def execute(page) - if page.update(@params[:content], format: @params[:format], message: @params[:message], last_commit_sha: @params[:last_commit_sha]) + if page.update(@params) execute_hooks(page, 'update') end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 652277e3b78..7027ac4b5db 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -30,7 +30,7 @@ class FileUploader < GitlabUploader # # Returns a String without a trailing slash def self.dynamic_path_segment(model) - File.join(CarrierWave.root, base_dir, model.path_with_namespace) + File.join(CarrierWave.root, base_dir, model.full_path) end attr_accessor :model diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 8bb2a563990..a4f49d3f6d7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -322,7 +322,7 @@ \. This setting requires a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. - = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') + = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index') .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 843c71af466..2aadc071c75 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -70,7 +70,7 @@ %span.badge = storage_counter(project.statistics.storage_size) %span.pull-right.light - %span.monospace= project.path_with_namespace + ".git" + %span.monospace= project.full_path + '.git' .panel-footer = paginate @projects, param_name: 'projects_page', theme: 'gitlab' @@ -88,7 +88,7 @@ %span.badge = storage_counter(project.statistics.storage_size) %span.pull-right.light - %span.monospace= project.path_with_namespace + ".git" + %span.monospace= project.full_path + '.git' .col-md-6 - if can?(current_user, :admin_group_member, @group) diff --git a/app/views/admin/health_check/_failing_storages.html.haml b/app/views/admin/health_check/_failing_storages.html.haml new file mode 100644 index 00000000000..6830201538d --- /dev/null +++ b/app/views/admin/health_check/_failing_storages.html.haml @@ -0,0 +1,15 @@ +- if failing_storages.any? + = _('There are problems accessing Git storage: ') + %ul + - failing_storages.each do |storage_health| + %li + = failing_storage_health_message(storage_health) + %ul + - storage_health.failing_circuit_breakers.each do |circuit_breaker| + %li + #{circuit_breaker.hostname}: #{message_for_circuit_breaker(circuit_breaker)} + + = _("Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again.") + .prepend-top-10 + = button_to _("Reset git storage health information"), reset_storage_health_admin_health_check_path, + method: :post, class: 'btn btn-default' diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index f16f59623f7..517db50b97f 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,22 +1,22 @@ - @no_container = true -- page_title "Health Check" +- page_title _('Health Check') +- no_errors = @errors.blank? && @failing_storage_statuses.blank? = render 'admin/monitoring/head' %div{ class: container_class } - %h3.page-title - Health Check + %h3.page-title= page_title .bs-callout.clearfix .pull-left %p - Access token is + #{ s_('HealthCheck|Access token is') } %code#health-check-token= current_application_settings.health_check_access_token .prepend-top-10 - = button_to "Reset health check access token", reset_health_check_token_admin_application_settings_path, + = button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path, method: :put, class: 'btn btn-default', - data: { confirm: 'Are you sure you want to reset the health check token?' } + data: { confirm: _('Are you sure you want to reset the health check token?') } %p.light - Health information can be retrieved from the following endpoints. More information is available - = link_to 'here', help_page_path('user/admin_area/monitoring/health_check') + #{ _('Health information can be retrieved from the following endpoints. More information is available') } + = link_to s_('More information is available|here'), help_page_path('user/admin_area/monitoring/health_check') %ul %li %code= readiness_url(token: current_application_settings.health_check_access_token) @@ -29,14 +29,15 @@ .panel.panel-default .panel-heading Current Status: - - if @errors.blank? + - if no_errors = icon('circle', class: 'cgreen') - Healthy + #{ s_('HealthCheck|Healthy') } - else = icon('warning', class: 'cred') - Unhealthy + #{ s_('HealthCheck|Unhealthy') } .panel-body - - if @errors.blank? - No Health Problems Detected + - if no_errors + #{ s_('HealthCheck|No Health Problems Detected') } - else = @errors + = render partial: 'failing_storages', object: @failing_storage_statuses diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index dfbc7772698..e6408f35201 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,6 +1,6 @@ - page_title "CI Lint" - page_description "Validate your GitLab CI configuration file" -- content_for :page_specific_javascripts do +- content_for :library_javascripts do = page_specific_javascript_tag('lib/ace.js') %h2 Check your .gitlab-ci.yml diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index ec6cb1a9624..c546252455a 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -13,10 +13,8 @@ - if show_callout?('user_callout_dismissed') = render 'shared/user_callout' - - if @projects.any? || params[:name] + - if has_projects_or_name?(@projects, params) = render 'dashboard/projects_head' - - - if @projects.any? || params[:name] = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index ae1d733a516..14f9f8cd70a 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -9,7 +9,7 @@ %div{ class: container_class } = render 'dashboard/projects_head' - - if @projects.any? || params[:filter_projects] + - if params[:filter_projects] || any_projects?(@projects) = render 'projects' - else %h3 You don't have starred projects yet diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 735d9390699..f83ebbf09ef 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -4,6 +4,10 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") +- content_for :page_specific_javascripts do + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'filtered_search' + - if show_new_nav? && group_issues_exists - content_for :breadcrumbs_extra do = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do @@ -20,7 +24,7 @@ Subscribe = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue" - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .row-content-block.second-block Only issues from the diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 56e628a2b74..b18b3dd5766 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -121,7 +121,7 @@ .key g .key p %td - Go to the project's home page + Go to the project's overview page %tr %td.shortcut .key g diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 48edbb8c16f..f18c3a74120 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -1,5 +1,7 @@ - page_title "UI Development Kit", "Help" - lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." +- content_for :page_specific_javascripts do + = webpack_bundle_tag('ui_development_kit') .gitlab-ui-dev-kit %h1 GitLab UI development kit @@ -407,29 +409,6 @@ .dropdown-content .dropdown-loading = icon('spinner spin') - :javascript - $('#js-project-dropdown').glDropdown({ - data: function (term, callback) { - Api.projects(term, { order_by: 'last_activity_at' }, function (data) { - callback(data); - }); - }, - text: function (project) { - return project.name_with_namespace || project.name; - }, - selectable: true, - fieldName: "author_id", - filterable: true, - search: { - fields: ['name_with_namespace'] - }, - id: function (data) { - return data.id; - }, - isSelected: function (data) { - return data.id === 2; - } - }) .example %div diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index 0e7f0b5ed4f..e9a04e6c122 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -25,7 +25,7 @@ %td = provider_project_link(provider, project.import_source) %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.full_path, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index fde671e25a9..4dc3a4a0acf 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -4,7 +4,7 @@ job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('#{link_to @project.path_with_namespace, project_path(@project)}') + target_field.append('#{link_to @project.full_path, project_path(@project)}') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started") - else diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index e6058617ac9..9589e0956f4 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -35,7 +35,7 @@ %td = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer' %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.full_path, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index 5de5da5e6a2..7b832c6a23a 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -33,7 +33,7 @@ %td = project.import_source %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.full_path, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 7456799ca0e..37734414835 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -28,7 +28,7 @@ %td = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.full_path, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 767dffb5589..008e8287aa3 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -1,25 +1,43 @@ - page_title "GitLab Import" - header_title "Projects", root_path +- content_for :page_specific_javascripts do + = webpack_bundle_tag 'project_import_gl' + %h3.page-title = icon('gitlab') Import an exported GitLab project %hr -= form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do - %p - Project will be imported as - %strong - #{@namespace.name}/#{@path} += form_tag import_gitlab_project_path, class: 'new_project', multipart: true do + .row + .form-group.col-xs-12.col-sm-6 + = label_tag :namespace_id, 'Project path', class: 'label-light' + .form-group + .input-group + - if current_user.can_select_namespace? + .input-group-addon + = root_url + = select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1 - %p - To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here. - .form-group - = hidden_field_tag :namespace_id, @namespace.id - = hidden_field_tag :path, @path - = label_tag :file, class: 'control-label' do - %span GitLab project export - .col-sm-10 - = file_field_tag :file, class: '' + - else + .input-group-addon.static-namespace + #{root_url}#{current_user.username}/ + = hidden_field_tag :namespace_id, value: current_user.namespace_id + .form-group.col-xs-12.col-sm-6.project-path + = label_tag :path, 'Project name', class: 'label-light' + = text_field_tag :path, nil, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true - .form-actions - = submit_tag 'Import project', class: 'btn btn-create' + .row + .form-group.col-md-12 + To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here. + .row + .form-group.col-sm-12 + = hidden_field_tag :namespace_id, @namespace.id + = hidden_field_tag :path, @path + = label_tag :file, 'GitLab project export', class: 'label-light' + .form-group + = file_field_tag :file, class: '' + .row + .form-actions + = submit_tag 'Import project', class: 'btn btn-create' + = link_to 'Cancel', new_project_path, class: 'btn btn-cancel' diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 60de6bfe816..bc61aeece72 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -38,7 +38,7 @@ %td = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer' %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.full_path, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span diff --git a/app/views/layouts/_bootlint.haml b/app/views/layouts/_bootlint.haml index 69280687a9d..d603a74c4e4 100644 --- a/app/views/layouts/_bootlint.haml +++ b/app/views/layouts/_bootlint.haml @@ -1,4 +1,5 @@ +-# haml-lint:disable InlineJavaScript :javascript - jQuery(document).ready(function() { - javascript:(function(){var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s)})(); - }); + window.onload = function() { + var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s); + } diff --git a/app/views/layouts/_google_analytics.html.haml b/app/views/layouts/_google_analytics.html.haml index 81e03c7eff2..98ea96b0b77 100644 --- a/app/views/layouts/_google_analytics.html.haml +++ b/app/views/layouts/_google_analytics.html.haml @@ -1,3 +1,4 @@ +-# haml-lint:disable InlineJavaScript :javascript var _gaq = _gaq || []; _gaq.push(['_setAccount', '#{extra_config.google_analytics_id}']); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 6ad22958df3..3babdae3968 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -38,6 +38,9 @@ = Gon::Base.render_data + - if content_for?(:library_javascripts) + = yield :library_javascripts + = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "locale" diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 4bb0dfc73fd..fe0ec35d003 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -2,7 +2,9 @@ - noteable_type = @noteable.class if @noteable.present? - if project + -# haml-lint:disable InlineJavaScript :javascript + gl = window.gl || {}; gl.GfmAutoComplete = gl.GfmAutoComplete || {}; gl.GfmAutoComplete.dataSources = { members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}", diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml index 259b4f7cdfc..a888e8ae187 100644 --- a/app/views/layouts/_piwik.html.haml +++ b/app/views/layouts/_piwik.html.haml @@ -1,4 +1,5 @@ <!-- Piwik --> +-# haml-lint:disable InlineJavaScript :javascript var _paq = _paq || []; _paq.push(['trackPageView']); diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index bc3293fd100..b32cfe158bb 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -44,7 +44,7 @@ = icon('tachometer fw') %li = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('hashtag fw') + = custom_icon('issues') - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index 60940dba475..2c1c23d6ea9 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -38,7 +38,7 @@ = icon('tachometer fw') %li = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('hashtag fw') + = custom_icon('issues') - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 8605380848d..261445ecd2b 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -25,7 +25,7 @@ %span Members - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit]) do + = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do = link_to edit_group_path(@group), title: 'Settings' do %span Settings diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml index 8db3e69aed4..0b4a9d92bea 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -1,16 +1,15 @@ -.nav-sidebar +.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } .context-header = link_to admin_root_path, title: 'Admin Overview' do .avatar-container.s40.settings-avatar = icon('wrench') .project-title Admin Area - = button_tag class: 'close-nav-button', type: 'button' do - %span.sr-only Close sidebar - = icon ('times') %ul.sidebar-top-level-items = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - %span + .nav-icon-container + = custom_icon('overview') + %span.nav-item-name Overview %ul.sidebar-sub-level-items @@ -45,7 +44,9 @@ = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do = link_to admin_conversational_development_index_path, title: 'Monitoring' do - %span + .nav-icon-container + = custom_icon('monitoring') + %span.nav-item-name Monitoring %ul.sidebar-sub-level-items @@ -76,52 +77,74 @@ = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do - %span + .nav-icon-container + = custom_icon('messages') + %span.nav-item-name Messages = nav_link(controller: [:hooks, :hook_logs]) do = link_to admin_hooks_path, title: 'Hooks' do - %span + .nav-icon-container + = custom_icon('system_hooks') + %span.nav-item-name System Hooks = nav_link(controller: :applications) do = link_to admin_applications_path, title: 'Applications' do - %span + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name Applications = nav_link(controller: :abuse_reports) do = link_to admin_abuse_reports_path, title: "Abuse Reports" do - %span - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + .nav-icon-container + = custom_icon('abuse_reports') + %span.nav-item-name Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - if akismet_enabled? = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path, title: "Spam Logs" do - %span + .nav-icon-container + = custom_icon('spam_logs') + %span.nav-item-name Spam Logs = nav_link(controller: :deploy_keys) do = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - %span + .nav-icon-container + = custom_icon('key') + %span.nav-item-name Deploy Keys = nav_link(controller: :services) do = link_to admin_application_settings_services_path, title: 'Service Templates' do - %span + .nav-icon-container + = custom_icon('service_templates') + %span.nav-item-name Service Templates = nav_link(controller: :labels) do = link_to admin_labels_path, title: 'Labels' do - %span + .nav-icon-container + = custom_icon('labels') + %span.nav-item-name Labels = nav_link(controller: :appearances) do = link_to admin_appearances_path, title: 'Appearances' do - %span + .nav-icon-container + = custom_icon('appearance') + %span.nav-item-name Appearance %li.divider = nav_link(controller: :application_settings) do = link_to admin_application_settings_path, title: 'Settings' do - %span + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name Settings + + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index 4fd9e213ead..c7dabbd8237 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -1,18 +1,17 @@ -.nav-sidebar +.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } .context-header = link_to group_path(@group), title: @group.name do .avatar-container.s40.group-avatar = image_tag group_icon(@group), class: "avatar s40 avatar-tile" .group-title = @group.name - = button_tag class: 'close-nav-button', type: 'button' do - %span.sr-only Close sidebar - = icon ('times') %ul.sidebar-top-level-items = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'About group' do - %span - About + = link_to group_path(@group), title: 'Group overview' do + .nav-icon-container + = custom_icon('project') + %span.nav-item-name + Overview %ul.sidebar-sub-level-items = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do @@ -27,10 +26,12 @@ = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do = link_to issues_group_path(@group), title: 'Issues' do - %span + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) Issues + %span.badge.count= number_with_delimiter(issues.count) %ul.sidebar-sub-level-items = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do @@ -50,18 +51,24 @@ = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - %span + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - %span.badge.count= number_with_delimiter(merge_requests.count) Merge Requests + %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(path: 'group_members#index') do = link_to group_group_members_path(@group), title: 'Members' do - %span + .nav-icon-container + = custom_icon('members') + %span.nav-item-name Members - if current_user && can?(current_user, :admin_group, @group) = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do = link_to edit_group_path(@group), title: 'Settings' do - %span + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name Settings %ul.sidebar-sub-level-items = nav_link(path: 'groups#edit') do @@ -75,6 +82,8 @@ Projects = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do + = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do %span - Pipelines + CI / CD + + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index 6bbd569583e..edae009a28e 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -1,61 +1,84 @@ -.nav-sidebar +.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } .context-header = link_to profile_path, title: 'Profile Settings' do .avatar-container.s40.settings-avatar = icon('user') .project-title User Settings - = button_tag class: 'close-nav-button', type: 'button' do - %span.sr-only Close sidebar - = icon ('times') %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do - %span + .nav-icon-container + = custom_icon('profile') + %span.nav-item-name Profile = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account' do - %span + .nav-icon-container + = custom_icon('account') + %span.nav-item-name Account - if current_application_settings.user_oauth_applications? = nav_link(controller: 'oauth/applications') do = link_to applications_profile_path, title: 'Applications' do - %span + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name Applications = nav_link(controller: :chat_names) do = link_to profile_chat_names_path, title: 'Chat' do - %span + .nav-icon-container + = custom_icon('chat') + %span.nav-item-name Chat = nav_link(controller: :personal_access_tokens) do = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - %span + .nav-icon-container + = custom_icon('access_tokens') + %span.nav-item-name Access Tokens = nav_link(controller: :emails) do = link_to profile_emails_path, title: 'Emails' do - %span + .nav-icon-container + = custom_icon('emails') + %span.nav-item-name Emails - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to edit_profile_password_path, title: 'Password' do - %span + .nav-icon-container + = custom_icon('lock') + %span.nav-item-name Password = nav_link(controller: :notifications) do = link_to profile_notifications_path, title: 'Notifications' do - %span + .nav-icon-container + = custom_icon('notifications') + %span.nav-item-name Notifications = nav_link(controller: :keys) do = link_to profile_keys_path, title: 'SSH Keys' do - %span + .nav-icon-container + = custom_icon('key') + %span.nav-item-name SSH Keys = nav_link(controller: :gpg_keys) do = link_to profile_gpg_keys_path, title: 'GPG Keys' do - %span + .nav-icon-container + = custom_icon('key_2') + %span.nav-item-name GPG Keys = nav_link(controller: :preferences) do = link_to profile_preferences_path, title: 'Preferences' do - %span + .nav-icon-container + = custom_icon('preferences') + %span.nav-item-name Preferences = nav_link(path: 'profiles#audit_log') do = link_to audit_log_profile_path, title: 'Authentication log' do - %span + .nav-icon-container + = custom_icon('authentication_log') + %span.nav-item-name Authentication log + + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index 00395b222e4..e0477c29ebe 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -1,4 +1,4 @@ -.nav-sidebar +.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - can_edit = can?(current_user, :admin_project, @project) .context-header = link_to project_path(@project), title: @project.name do @@ -6,14 +6,13 @@ = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') .project-title = @project.name - = button_tag class: 'close-nav-button', type: 'button' do - %span.sr-only Close sidebar - = icon ('times') %ul.sidebar-top-level-items = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'About project', class: 'shortcuts-project' do - %span - About + = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do + .nav-icon-container + = custom_icon('project') + %span.nav-item-name + Overview %ul.sidebar-sub-level-items = nav_link(path: 'projects#show') do @@ -32,7 +31,9 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do - %span + .nav-icon-container + = custom_icon('doc_text') + %span.nav-item-name Repository %ul.sidebar-sub-level-items @@ -71,59 +72,58 @@ - if project_nav_tab? :container_registry = nav_link(controller: %w[projects/registry/repositories]) do = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - %span + .nav-icon-container + = custom_icon('container_registry') + %span.nav-item-name Registry - if project_nav_tab? :issues = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - %span - - if @project.issues_enabled? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name Issues + - if @project.issues_enabled? + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) %ul.sidebar-sub-level-items - - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), title: 'Issues' do - %span - List - - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do - %span - Board + = nav_link(controller: :issues) do + = link_to project_issues_path(@project), title: 'Issues' do + %span + List - - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) - = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests' do - %span - Merge Requests + = nav_link(controller: :boards) do + = link_to project_boards_path(@project), title: 'Board' do + %span + Board - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: 'Labels' do + %span + Labels - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: 'Milestones' do + %span + Milestones - if project_nav_tab? :merge_requests = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - %span - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name Merge Requests + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines + = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do + .nav-icon-container + = custom_icon('pipeline') + %span.nav-item-name + CI / CD %ul.sidebar-sub-level-items - if project_nav_tab? :pipelines @@ -159,25 +159,31 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - %span + .nav-icon-container + = custom_icon('wiki') + %span.nav-item-name Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - %span + .nav-icon-container + = custom_icon('snippets') + %span.nav-item-name Snippets - if project_nav_tab? :settings = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do - %span + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name Settings %ul.sidebar-sub-level-items - can_edit = can?(current_user, :admin_project, @project) - if can_edit - = nav_link(controller: :projects) do + = nav_link(path: %w[projects#edit]) do = link_to edit_project_path(@project), title: 'General' do %span General @@ -196,9 +202,9 @@ Repository - if @project.feature_available?(:builds, current_user) = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do + = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do %span - Pipelines + CI / CD - if Gitlab.config.pages.enabled = nav_link(controller: :pages) do = link_to project_pages_path(@project), title: 'Pages' do @@ -207,9 +213,13 @@ - else = nav_link(path: %w[members#show]) do - = link_to project_settings_members_path(@project), title: 'Settings', class: 'shortcuts-tree' do - %span - Settings + = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + + = render 'shared/sidebar_toggle_button' -# Shortcut to Project > Activity %li.hidden diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 99adb83cd1f..54d56e9b873 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -10,6 +10,7 @@ - content_for :project_javascripts do - project = @target_project || @project - if current_user + -# haml-lint:disable InlineJavaScript :javascript window.uploads_path = "#{project_uploads_path(project)}"; diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index 57971205e0e..849075a0ba5 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -2,6 +2,7 @@ - content_for :page_specific_javascripts do - if @snippet && current_user + -# haml-lint:disable InlineJavaScript :javascript window.uploads_path = "#{upload_path('personal_snippet', id: @snippet.id)}"; diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index cf750378e25..2216708d354 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -1,5 +1,6 @@ - page_title "Personal Access Tokens" - @content_class = "limit-container-width" unless fluid_layout + = render 'profiles/head' .row.prepend-top-default @@ -19,7 +20,7 @@ %h5.prepend-top-0 Your New Personal Access Token .form-group - = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" + = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" = clipboard_button(text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. @@ -28,8 +29,3 @@ = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens - -:javascript - $("#created-personal-access-token").click(function() { - this.select(); - }); diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 9aed498a8a0..f08dcc0c242 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -60,9 +60,9 @@ = f.select :dashboard, dashboard_choices, {}, class: 'form-control' .form-group = f.label :project_view, class: 'label-light' do - Project home page content + Project overview content = f.select :project_view, project_view_choices, {}, class: 'form-control' .help-block - Choose what content you want to see on a project’s home page + Choose what content you want to see on a project’s overview page .form-group = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 037cb30efb9..33e062c1c9c 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -7,97 +7,92 @@ = render 'profiles/head' -- if inject_u2f_api? - - content_for :page_specific_javascripts do +- content_for :page_specific_javascripts do + - if inject_u2f_api? = page_specific_javascript_bundle_tag('u2f') + = page_specific_javascript_bundle_tag('two_factor_auth') -.row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0 - Register Two-Factor Authentication App - %p - Use an app on your mobile device to enable two-factor authentication (2FA). - .col-lg-8 - - if current_user.two_factor_otp_enabled? - = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." - - else +.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path } + .row.prepend-top-default + .col-lg-4 + %h4.prepend-top-0 + Register Two-Factor Authentication App %p - Download the Google Authenticator application from App Store or Google Play Store and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. - .row.append-bottom-10 - .col-md-4 - = raw @qr_code - .col-md-8 - .account-well - %p.prepend-top-0.append-bottom-0 - Can't scan the code? - %p.prepend-top-0.append-bottom-0 - To add the entry manually, provide the following details to the application on your phone. - %p.prepend-top-0.append-bottom-0 - Account: - = @account_string - %p.prepend-top-0.append-bottom-0 - Key: - = current_user.otp_secret.scan(/.{4}/).join(' ') - %p.two-factor-new-manual-content - Time based: Yes - = form_tag profile_two_factor_auth_path, method: :post do |f| - - if @error - .alert.alert-danger - = @error - .form-group - = label_tag :pin_code, nil, class: "label-light" - = text_field_tag :pin_code, nil, class: "form-control", required: true - .prepend-top-default - = submit_tag 'Register with two-factor app', class: 'btn btn-success' + Use an app on your mobile device to enable two-factor authentication (2FA). + .col-lg-8 + - if current_user.two_factor_otp_enabled? + = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." + - else + %p + Download the Google Authenticator application from App Store or Google Play Store and scan this code. + More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. + .row.append-bottom-10 + .col-md-4 + = raw @qr_code + .col-md-8 + .account-well + %p.prepend-top-0.append-bottom-0 + Can't scan the code? + %p.prepend-top-0.append-bottom-0 + To add the entry manually, provide the following details to the application on your phone. + %p.prepend-top-0.append-bottom-0 + Account: + = @account_string + %p.prepend-top-0.append-bottom-0 + Key: + = current_user.otp_secret.scan(/.{4}/).join(' ') + %p.two-factor-new-manual-content + Time based: Yes + = form_tag profile_two_factor_auth_path, method: :post do |f| + - if @error + .alert.alert-danger + = @error + .form-group + = label_tag :pin_code, nil, class: "label-light" + = text_field_tag :pin_code, nil, class: "form-control", required: true + .prepend-top-default + = submit_tag 'Register with two-factor app', class: 'btn btn-success' -%hr + %hr -.row.prepend-top-default - - .col-lg-4 - %h4.prepend-top-0 - Register Universal Two-Factor (U2F) Device - %p - Use a hardware device to add the second factor of authentication. - %p - As U2F devices are only supported by a few browsers, we require that you set up a - two-factor authentication app before a U2F device. That way you'll always be able to - log in - even when you're using an unsupported browser. - .col-lg-8 - - if @u2f_registration.errors.present? - = form_errors(@u2f_registration) - = render "u2f/register" + .row.prepend-top-default + .col-lg-4 + %h4.prepend-top-0 + Register Universal Two-Factor (U2F) Device + %p + Use a hardware device to add the second factor of authentication. + %p + As U2F devices are only supported by a few browsers, we require that you set up a + two-factor authentication app before a U2F device. That way you'll always be able to + log in - even when you're using an unsupported browser. + .col-lg-8 + - if @u2f_registration.errors.present? + = form_errors(@u2f_registration) + = render "u2f/register" - %hr + %hr - %h5 U2F Devices (#{@u2f_registrations.length}) + %h5 U2F Devices (#{@u2f_registrations.length}) - - if @u2f_registrations.present? - .table-responsive - %table.table.table-bordered.u2f-registrations - %colgroup - %col{ width: "50%" } - %col{ width: "30%" } - %col{ width: "20%" } - %thead - %tr - %th Name - %th Registered On - %th - %tbody - - @u2f_registrations.each do |registration| + - if @u2f_registrations.present? + .table-responsive + %table.table.table-bordered.u2f-registrations + %colgroup + %col{ width: "50%" } + %col{ width: "30%" } + %col{ width: "20%" } + %thead %tr - %td= registration.name.presence || "<no name set>" - %td= registration.created_at.to_date.to_s(:medium) - %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." } - - - else - .settings-message.text-center - You don't have any U2F devices registered yet. - + %th Name + %th Registered On + %th + %tbody + - @u2f_registrations.each do |registration| + %tr + %td= registration.name.presence || "<no name set>" + %td= registration.created_at.to_date.to_s(:medium) + %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." } -- if two_factor_skippable? - :javascript - var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>"; - $(".flash-alert").append(button); + - else + .settings-message.text-center + You don't have any U2F devices registered yet. diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index ecc966ed453..ad63f5e73ae 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -8,9 +8,3 @@ .content_list.project-activity{ :"data-href" => activity_project_path(@project) } = spinner - -:javascript - var activity = new gl.Activities(); - $(document).on('page:restore', function (event) { - activity.reloadActivities() - }) diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 426085b3e1c..398d9306bed 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -1,7 +1,7 @@ - commit = local_assigns.fetch(:commit) { @repository.commit } - ref = local_assigns.fetch(:ref) { current_ref } - project = local_assigns.fetch(:project) { @project } -#tree-holder.tree-holder.clearfix +.tree-holder.clearfix .nav-block = render 'projects/tree/tree_header', tree: @tree diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 818010bc7d3..cc5afa943cf 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,8 +1,3 @@ - form = local_assigns.fetch(:form) -%fieldset.features.merge-requests-feature.append-bottom-default - %hr - %h5.prepend-top-0 - Merge Requests - - = render 'projects/merge_request_merge_settings', form: form += render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml new file mode 100644 index 00000000000..21baf35f2ac --- /dev/null +++ b/app/views/projects/_project_templates.html.haml @@ -0,0 +1,10 @@ +.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } } + .btn.blank-option.active + %input{ type: "radio", autocomplete: "off", name: "project_templates", id: "blank", checked: "true" } + = icon('file-o', class: 'btn-template-icon') + Blank + - Gitlab::ProjectTemplate.all.each do |template| + .btn + %input{ type: "radio", autocomplete: "off", name: "project_templates", id: template.name } + = custom_icon(template.logo) + = template.title diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml index 18e86ac5a92..b85bbcb980e 100644 --- a/app/views/projects/artifacts/file.html.haml +++ b/app/views/projects/artifacts/file.html.haml @@ -3,7 +3,7 @@ = render "projects/jobs/header", show_controls: false -#tree-holder.tree-holder +.tree-holder .nav-block %ul.breadcrumb.repo-breadcrumb %li diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index b2959ef6d31..03ab1bb59e4 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -20,6 +20,3 @@ - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 = commit_in_fork_help - -:javascript - new NewCommitForm($('.js-create-dir-form')) diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index 6a4a657fa8c..750bdef3308 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -13,6 +13,3 @@ .col-sm-offset-2.col-sm-10 = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - -:javascript - new NewCommitForm($('.js-delete-blob-form')) diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 7dd834e84b5..278f4201bba 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -10,7 +10,7 @@ = render 'projects/last_push' %div{ class: container_class } - #tree-holder.tree-holder + .tree-holder = render 'blob', blob: @blob - if can_modify_blob?(@blob) diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 03eefcc2b4d..2baaaf6ac5b 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -28,8 +28,4 @@ .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' - -:javascript - var availableRefs = #{@project.repository.ref_names.to_json}; - - new NewBranchForm($('.js-create-branch-form'), availableRefs) +%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 419fbe99af8..09bcd187e59 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,4 +1,4 @@ -.page-content-header +.page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) } .header-main-content = render partial: 'signature', object: @commit.signature %strong @@ -79,6 +79,3 @@ = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' in = time_interval_in_words last_pipeline.duration - -:javascript - $(".commit-info.branches").load("#{branches_project_commit_path(@project, @commit.id)}"); diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 12b73ecdf13..e7da47032be 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -5,7 +5,7 @@ - notes = commit.notes - note_count = notes.user.count -- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits)] +- cache_key = [project.full_path, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits)] - cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index bd2d900997e..7ae56086177 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -11,34 +11,32 @@ = content_for :sub_nav do = render "head" -%div{ class: container_class } - .tree-holder - .nav-block - .tree-ref-container - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'commits' +.js-project-commits-show{ 'data-commits-limit' => @limit } + %div{ class: container_class } + .tree-holder + .nav-block + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'commits' + + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + .tree-controls.hidden-xs.hidden-sm + - if @merge_request.present? + .control + = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn' + - elsif create_mr_button?(@repository.root_ref, @ref) + .control + = link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs - .tree-controls.hidden-xs.hidden-sm - - if @merge_request.present? .control - = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn' - - elsif create_mr_button?(@repository.root_ref, @ref) + = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do + = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } .control - = link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' + = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do + = icon("rss") - .control - = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do - = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - .control - = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do - = icon("rss") - - %div{ id: dom_id(@project) } - %ol#commits-list.list-unstyled.content_list - = render 'commits', project: @project, ref: @ref - = spinner - -:javascript - CommitsList.init(#{@limit}); + %div{ id: dom_id(@project) } + %ol#commits-list.list-unstyled.content_list + = render 'commits', project: @project, ref: @ref + = spinner diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f9385459a66..8c8aa4c78f5 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files -.content-block.oneline-block.files-changed +.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .inline-parallel-buttons - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index e69c7f20d49..efc0ea31917 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,36 +1,34 @@ -.js-toggle-container - .commit-stat-summary - Showing - %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" } - %strong= pluralize(diff_files.size, "changed file") +- sum_added_lines = diff_files.sum(&:added_lines) +- sum_removed_lines = diff_files.sum(&:removed_lines) +.commit-stat-summary.dropdown + Showing + %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }< + = pluralize(diff_files.size, "changed file") + = icon("caret-down", class: "prepend-left-5") + %span.diff-stats-additions-deletions-expanded#diff-stats with - %strong.cgreen #{diff_files.sum(&:added_lines)} additions + %strong.cgreen #{sum_added_lines} additions and - %strong.cred #{diff_files.sum(&:removed_lines)} deletions - .file-stats.js-toggle-content.hide - %ul - - diff_files.each do |diff_file| - - file_hash = hexdigest(diff_file.file_path) - %li - - if diff_file.deleted_file? - %span.deleted-file - %a{ href: "##{file_hash}" } - %i.fa.fa-minus - = diff_file.old_path - - elsif diff_file.renamed_file? - %span.renamed-file - %a{ href: "##{file_hash}" } - %i.fa.fa-minus - = diff_file.old_path - → - = diff_file.new_path - - elsif diff_file.new_file? - %span.new-file - %a{ href: "##{file_hash}" } - %i.fa.fa-plus - = diff_file.new_path - - else - %span.edit-file - %a{ href: "##{file_hash}" } - %i.fa.fa-adjust - = diff_file.new_path + %strong.cred #{sum_removed_lines} deletions + .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" } + %strong.cgreen< + +#{sum_added_lines} + %strong.cred< + \-#{sum_removed_lines} + .dropdown-menu.diff-file-changes + = dropdown_filter("Search files") + .dropdown-content + %ul + - diff_files.each do |diff_file| + %li + %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } + = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") + %span.diff-file-changes-path= diff_file.new_path + .pull-right + %span.cgreen< + +#{diff_file.added_lines} + %span.cred< + \-#{diff_file.removed_lines} + %li.dropdown-menu-empty-link.hidden + %a{ href: "#" } + No files found. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 087cb804449..20fceda26dc 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,12 +1,19 @@ +- page_title "General" - @content_class = "limit-container-width" unless fluid_layout +- expanded = Rails.env.test? = render "projects/settings/head" + .project-edit-container - .row.prepend-top-default - .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0 - Project settings - .col-lg-8 + %section.settings.general-settings + .settings-header + %h4 + General project settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Update your project name, description, avatar, and other general settings. + .settings-content.no-animate{ class: ('expanded' if expanded) } .project-edit-errors = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| %fieldset @@ -35,89 +42,7 @@ = f.label :tag_list, "Tags", class: 'label-light' = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" %p.help-block Separate tags with commas. - %hr - %fieldset - %h5.prepend-top-0 - Sharing & Permissions - .form_group.prepend-top-20.sharing-and-permissions - .row.js-visibility-select - .col-md-8 - .label-light - = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - %span.help-block - .col-md-4.visibility-select-container - = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) - = f.fields_for :project_feature do |feature_fields| - %fieldset.features - .row - .col-md-8.project-feature - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block View and edit files in this project - .col-md-4.js-repo-access-level - = project_feature_access_select(:repository_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-4 - = project_feature_access_select(:merge_requests_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' - %span.help-block Build, test, and deploy your changes - .col-md-4 - = project_feature_access_select(:builds_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-4 - = project_feature_access_select(:snippets_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-4 - = project_feature_access_select(:issues_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-4 - = project_feature_access_select(:wiki_access_level) - .form-group - = render 'shared/allow_request_access', form: f - - if Gitlab.config.lfs.enabled && current_user.admin? - .row.js-lfs-enabled - .col-md-8 - = f.label :lfs_enabled, 'LFS', class: 'label-light' - %span.help-block - Git Large File Storage - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - .col-md-4 - .select-wrapper - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } - = icon('chevron-down') - - if Gitlab.config.registry.enabled - .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' - - = render 'merge_request_settings', form: f - - %hr - %fieldset.features.append-bottom-default + %fieldset.features %h5.prepend-top-0 Project avatar .form-group @@ -137,41 +62,114 @@ = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" - .row.prepend-top-default - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0 - Housekeeping - %p.append-bottom-0 - %p - Runs a number of housekeeping tasks within the current repository, - such as compressing file revisions and removing unreachable objects. - .col-lg-8 - = link_to 'Housekeeping', housekeeping_project_path(@project), - method: :post, class: "btn btn-default" - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0 - Export project - %p.append-bottom-0 - %p - Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. - %p - Once the exported file is ready, you will receive a notification email with a download link. + %section.settings.sharing-permissions + .settings-header + %h4 + Sharing and permissions + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Enable or disable certain project features and choose access levels. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| + .form_group.sharing-and-permissions + .row.js-visibility-select + .col-md-8 + .label-light + = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level + = link_to icon('question-circle'), help_page_path("public_access/public_access") + %span.help-block + .col-md-4.visibility-select-container + = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) + = f.fields_for :project_feature do |feature_fields| + %fieldset.features + .row + .col-md-8.project-feature + = feature_fields.label :repository_access_level, "Repository", class: 'label-light' + %span.help-block View and edit files in this project + .col-md-4.js-repo-access-level + = project_feature_access_select(:repository_access_level) - .col-lg-8 + .row + .col-md-8.project-feature.nested + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-4 + = project_feature_access_select(:merge_requests_access_level) - - if @project.export_project_path - = link_to 'Download export', download_export_project_path(@project), - rel: 'nofollow', download: '', method: :get, class: "btn btn-default" - = link_to 'Generate new export', generate_new_export_project_path(@project), - method: :post, class: "btn btn-default" - - else - = link_to 'Export project', export_project_path(@project), - method: :post, class: "btn btn-default" + .row + .col-md-8.project-feature.nested + = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' + %span.help-block Build, test, and deploy your changes + .col-md-4 + = project_feature_access_select(:builds_access_level) + + .row + .col-md-8.project-feature + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-4 + = project_feature_access_select(:snippets_access_level) + + .row + .col-md-8.project-feature + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-4 + = project_feature_access_select(:issues_access_level) + + .row + .col-md-8.project-feature + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-4 + = project_feature_access_select(:wiki_access_level) + .form-group + = render 'shared/allow_request_access', form: f + - if Gitlab.config.lfs.enabled && current_user.admin? + .row.js-lfs-enabled.form-group.sharing-and-permissions + .col-md-8 + = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light' + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + %span.help-block Manages large files such as audio, video and graphics files. + .col-md-4 + .select-wrapper + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } + = icon('chevron-down') + - if Gitlab.config.registry.enabled + .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry + %br + %span.descr Enable Container Registry for this project + = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + = f.submit 'Save changes', class: "btn btn-save" + + + %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } + .settings-header + %h4 + Merge request settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Customize your merge request restrictions. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| + = render 'merge_request_settings', form: f + = f.submit 'Save changes', class: "btn btn-save" + %section.settings + .settings-header + %h4 + Export project + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + .settings-content.no-animate{ class: ('expanded' if expanded) } .bs-callout.bs-callout-info %p.append-bottom-0 %p @@ -189,110 +187,117 @@ %li Container registry images %li CI variables %li Any encrypted tokens - - if can? current_user, :archive_project, @project - %hr - .row.prepend-top-default - .col-lg-4 - %h4.warning-title.prepend-top-0 - - if @project.archived? - Unarchive project - - else - Archive project - %p.append-bottom-0 + %p + Once the exported file is ready, you will receive a notification email with a download link. + - if @project.export_project_path + = link_to 'Download export', download_export_project_path(@project), + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" + = link_to 'Generate new export', generate_new_export_project_path(@project), + method: :post, class: "btn btn-default" + - else + = link_to 'Export project', export_project_path(@project), + method: :post, class: "btn btn-default" + + %section.settings.advanced-settings + .settings-header + %h4 + Advanced settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .sub-section + %h4 Housekeeping + %p + Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects. + = link_to 'Run housekeeping', housekeeping_project_path(@project), + method: :post, class: "btn btn-default" + - if can? current_user, :archive_project, @project + .sub-section + %h4.warning-title + - if @project.archived? + Unarchive project + - else + Archive project - if @project.archived? - Unarchiving the project will mark its repository as active. The project can be committed to. + %p + Unarchiving the project will mark its repository as active. The project can be committed to. + %strong Once active this project shows up in the search and on the dashboard. + = link_to 'Unarchive project', unarchive_project_path(@project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" - else - Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. - .col-lg-8 - - if @project.archived? - %p - %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive project', unarchive_project_path(@project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-success" - - else - %p - %strong Archived projects cannot be committed to! - = link_to 'Archive project', archive_project_path(@project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0.warning-title - Rename repository - .col-lg-8 - = render 'projects/errors' - = form_for([@project.namespace.becomes(Namespace), @project]) do |f| - .form-group.project_name_holder - = f.label :name, class: 'label-light' do - Project name - .form-group - = f.text_field :name, class: "form-control" - .form-group - = f.label :path, class: 'label-light' do - %span Path - .form-group - .input-group - .input-group-addon - #{URI.join(root_url, @project.namespace.full_path)}/ - = f.text_field :path, class: 'form-control' - %ul - %li Be careful. Renaming a project's repository can have unintended side effects. - %li You will need to update your local repositories to point to the new location. - - if @project.deployment_services.any? - %li Your deployment services will be broken, you will need to manually fix the services after renaming. - = f.submit 'Rename project', class: "btn btn-warning" - - if can?(current_user, :change_namespace, @project) - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0.danger-title - Transfer project to new group - %p.append-bottom-0 - Please select the group you want to transfer this project to in the dropdown to the right. - .col-lg-8 - = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| + %p + Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. + %strong Archived projects cannot be committed to! + = link_to 'Archive project', archive_project_path(@project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" + .sub-section.rename-respository + %h4.warning-title + Rename repository + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + = render 'projects/errors' + = form_for([@project.namespace.becomes(Namespace), @project]) do |f| + .form-group.project_name_holder + = f.label :name, class: 'label-light' do + Project name + .form-group + = f.text_field :name, class: "form-control" .form-group - = label_tag :new_namespace_id, nil, class: 'label-light' do - %span Select a new namespace + = f.label :path, class: 'label-light' do + %span Path .form-group - = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' + .input-group + .input-group-addon + #{URI.join(root_url, @project.namespace.full_path)}/ + = f.text_field :path, class: 'form-control' %ul - %li Be careful. Changing the project's namespace can have unintended side effects. - %li You can only transfer the project to namespaces you manage. + %li Be careful. Renaming a project's repository can have unintended side effects. %li You will need to update your local repositories to point to the new location. - %li Project visibility level will be changed to match namespace rules when transfering to a group. - = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - - if @project.forked? && can?(current_user, :remove_fork_project, @project) - %hr - .row.prepend-top-default.append-bottom-default - .col-lg-4 - %h4.prepend-top-0.danger-title - Remove fork relationship - %p.append-bottom-0 + - if @project.deployment_services.any? + %li Your deployment services will be broken, you will need to manually fix the services after renaming. + = f.submit 'Rename project', class: "btn btn-warning" + - if can?(current_user, :change_namespace, @project) + .sub-section + %h4.danger-title + Transfer project + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| + .form-group + = label_tag :new_namespace_id, nil, class: 'label-light' do + %span Select a new namespace + .form-group + = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' + %ul + %li Be careful. Changing the project's namespace can have unintended side effects. + %li You can only transfer the project to namespaces you manage. + %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + .sub-section + %h4.danger-title + Remove fork relationship %p This will remove the fork relationship to source project = succeed "." do = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) - .col-lg-8 - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| - %p - %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - - if can?(current_user, :remove_project, @project) - %hr - .row.prepend-top-default.append-bottom-default - .col-lg-4 - %h4.prepend-top-0.danger-title - Remove project - %p.append-bottom-0 - Removing the project will delete its repository and all related resources including issues, merge requests etc. - .col-lg-8 - = form_tag(project_path(@project), method: :delete) do + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| + %p + %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + - if can?(current_user, :remove_project, @project) + .sub-section + %h4.danger-title + Remove project %p - %strong Removed projects cannot be restored! - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + Removing the project will delete its repository and all related resources including issues, merge requests etc. + = form_tag(project_path(@project), method: :delete) do + %p + %strong Removed projects cannot be restored! + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } .save-project-loader.hide .center diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index e3bf48ee47f..021575160ea 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -1,7 +1,7 @@ - page_title "Find File", @ref = render "projects/commits/head" -.file-finder-holder.tree-holder.clearfix +.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, @options.merge(format: :json)))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @id || @commit.id)) } .nav-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'find_file', path: @path @@ -17,11 +17,3 @@ %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" } %tbody = spinner nil, true - -:javascript - var projectFindFile = new ProjectFindFile($(".file-finder-holder"), { - url: "#{escape_javascript(project_files_path(@project, @ref, @options.merge(format: :json)))}", - treeUrl: "#{escape_javascript(project_tree_path(@project, @ref))}", - blobUrlTemplate: "#{escape_javascript(project_blob_path(@project, @id || @commit.id))}" - }); - new ShortcutsFindFile(projectFindFile); diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 249b9d82ad9..228c8c84792 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -3,8 +3,9 @@ - if show_new_nav? - add_to_breadcrumbs("Repository", project_tree_path(@project)) - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_d3') - = page_specific_javascript_bundle_tag('graphs') + = webpack_bundle_tag('common_d3') + = webpack_bundle_tag('graphs') + = webpack_bundle_tag('graphs_charts') = render "projects/commits/head" .repo-charts{ class: container_class } @@ -75,55 +76,10 @@ Commits per day hour (UTC) %canvas#hour-chart -:javascript - var responsiveChart = function (selector, data) { - var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }; - // get selector by context - var ctx = selector.get(0).getContext("2d"); - // pointing parent container to make chart.js inherit its width - var container = $(selector).parent(); - var generateChart = function() { - selector.attr('width', $(container).width()); - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8 - } - return new Chart(ctx).Bar(data, options); - }; - // enabling auto-resizing - $(window).resize(generateChart); - return generateChart(); - }; - - var chartData = function (keys, values) { - var data = { - labels : keys, - datasets : [{ - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data : values - }] - }; - return data; - }; - - var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}); - responsiveChart($('#hour-chart'), hourData); - - var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}); - responsiveChart($('#weekday-chart'), dayData); - - var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}); - responsiveChart($('#month-chart'), monthData); - - var data = #{@languages.to_json}; - var ctx = $("#languages-chart").get(0).getContext("2d"); - var options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false - } - var myPieChart = new Chart(ctx).Pie(data, options); +%script#projectChartData{ type: "application/json" } + - projectChartData = {}; + - projectChartData['hour'] = { 'keys' => @commits_per_time.keys, 'values' => @commits_per_time.values } + - projectChartData['weekDays'] = { 'keys' => @commits_per_week_days.keys, 'values' => @commits_per_week_days.values } + - projectChartData['month'] = { 'keys' => @commits_per_month.keys, 'values' => @commits_per_month.values } + - projectChartData['languages'] = @languages + = projectChartData.to_json.html_safe diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 4256a8c4d7e..f41a0d8293b 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,15 +1,16 @@ - @no_container = true - page_title "Contributors" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_d3') - = page_specific_javascript_bundle_tag('graphs') + = webpack_bundle_tag('common_d3') + = webpack_bundle_tag('graphs') + = webpack_bundle_tag('graphs_show') - if show_new_nav? - add_to_breadcrumbs("Repository", project_tree_path(@project)) = render 'projects/commits/head' -%div{ class: container_class } +.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .sub-header-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs' @@ -33,24 +34,3 @@ #contributors-master #contributors.clearfix %ol.contributors-list.clearfix - - - -:javascript - $.ajax({ - type: "GET", - url: "#{project_graph_path(@project, current_ref, format: :json)}", - dataType: "json", - success: function (data) { - var graph = new ContributorsStatGraph(); - graph.init(data); - - $("#brush_change").change(function(){ - graph.change_date_header(); - graph.redraw_authors(); - }); - - $(".stat-graph").fadeIn(); - $(".loading-graph").hide(); - } - }); diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index c52b3860636..8c490773a56 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -10,5 +10,3 @@ - if @project.external_import? %p.monospace git clone --bare #{@project.safe_import_url} %p Please wait while we import the repository for you. Refresh at will. - :javascript - new ProjectImport(); diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index f2db71e8838..99f4b30d085 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -1,101 +1,101 @@ - builds = @build.pipeline.builds.to_a %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } - .blocks-container - .block - %strong - = @build.name - %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' } - = icon('angle-double-right') - - #js-details-block-vue - - - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) + .sidebar-container + .blocks-container .block - .title - Job artifacts - - if @build.artifacts_expired? - %p.build-detail-row - The artifacts were removed - #{time_ago_with_tooltip(@build.artifacts_expire_at)} - - elsif @build.has_expiring_artifacts? - %p.build-detail-row - The artifacts will be removed in - %span.js-artifacts-remove= @build.artifacts_expire_at + %strong + = @build.name + %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' } + = icon('angle-double-right') - - if @build.artifacts? - .btn-group.btn-group-justified{ role: :group } - - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) - = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do - Keep + #js-details-block-vue - = link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do - Download + - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) + .block + .title + Job artifacts + - if @build.artifacts_expired? + %p.build-detail-row + The artifacts were removed + #{time_ago_with_tooltip(@build.artifacts_expire_at)} + - elsif @build.has_expiring_artifacts? + %p.build-detail-row + The artifacts will be removed in + %span.js-artifacts-remove= @build.artifacts_expire_at - - if @build.artifacts_metadata? - = link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do - Browse + - if @build.artifacts? + .btn-group.btn-group-justified{ role: :group } + - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) + = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do + Keep - - if @build.trigger_request - .build-widget.block - %h4.title - Trigger + = link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do + Download - %p - %span.build-light-text Token: - #{@build.trigger_request.trigger.short_token} + - if @build.artifacts_metadata? + = link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do + Browse + + - if @build.trigger_request + .build-widget.block + %h4.title + Trigger - - if @build.trigger_request.variables %p - %button.btn.group.btn-group-justified.reveal-variables Reveal Variables + %span.build-light-text Token: + #{@build.trigger_request.trigger.short_token} + - if @build.trigger_request.variables + %p + %button.btn.group.btn-group-justified.reveal-variables Reveal Variables - - @build.trigger_request.variables.each do |key, value| - .hide.js-build - .js-build-variable.trigger-build-variable= key - .js-build-value.trigger-build-value= value + %dl.js-build-variables.trigger-build-variables.hide + - @build.trigger_request.variables.each do |key, value| + %dt.js-build-variable.trigger-build-variable= key + %dd.js-build-value.trigger-build-value= value - %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") } - %p - Commit - = link_to @build.pipeline.short_sha, project_commit_path(@project, @build.pipeline.sha), class: 'commit-sha link-commit' - = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard") - - if @build.merge_request - in - = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit' + %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") } + %p + Commit + = link_to @build.pipeline.short_sha, project_commit_path(@project, @build.pipeline.sha), class: 'commit-sha link-commit' + = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard") + - if @build.merge_request + in + = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit' - %p.build-light-text.append-bottom-0 - #{@build.pipeline.git_commit_title} + %p.build-light-text.append-bottom-0 + #{@build.pipeline.git_commit_title} - - if @build.pipeline.stages_count > 1 - .dropdown.build-dropdown - %div - %span{ class: "ci-status-icon-#{@build.pipeline.status}" } - = ci_icon_for_status(@build.pipeline.status) - Pipeline - = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit' - from - = link_to "#{@build.pipeline.ref}", project_branch_path(@project, @build.pipeline.ref), class: 'link-commit' - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.stage-selection More - = icon('chevron-down') - %ul.dropdown-menu - - @build.pipeline.legacy_stages.each do |stage| - %li - %a.stage-item= stage.name + - if @build.pipeline.stages_count > 1 + .block-last.dropdown.build-dropdown + %div + %span{ class: "ci-status-icon-#{@build.pipeline.status}" } + = ci_icon_for_status(@build.pipeline.status) + Pipeline + = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit' + from + = link_to "#{@build.pipeline.ref}", project_branch_path(@project, @build.pipeline.ref), class: 'link-commit' + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.stage-selection More + = icon('chevron-down') + %ul.dropdown-menu + - @build.pipeline.legacy_stages.each do |stage| + %li + %a.stage-item= stage.name - .builds-container - - HasStatus::ORDERED_STATUSES.each do |build_status| - - builds.select{|build| build.status == build_status}.each do |build| - .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - = link_to project_job_path(@project, build) do - = icon('arrow-right') - %span{ class: "ci-status-icon-#{build.status}" } - = ci_icon_for_status(build.status) - %span - - if build.name - = build.name - - else - = build.id - - if build.retried? - %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } + .builds-container + - HasStatus::ORDERED_STATUSES.each do |build_status| + - builds.select{|build| build.status == build_status}.each do |build| + .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } + = link_to project_job_path(@project, build) do + = icon('arrow-right') + %span{ class: "ci-status-icon-#{build.status}" } + = ci_icon_for_status(build.status) + %span + - if build.name + = build.name + - else + = build.id + - if build.retried? + %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 3bdb5d0adc4..20acd476f73 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -33,7 +33,7 @@ Suggestions: %code= 'gitlab' %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace + %code= @project.full_path %p Reserved: = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 8958b2cf5e1..9d5cebdda53 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -41,7 +41,7 @@ - projects = target_projects(@project) .merge-request-select.dropdown = f.hidden_field :target_project_id - = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } + = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } .dropdown-menu.dropdown-menu-selectable.dropdown-target-project = dropdown_title("Select target project") = dropdown_filter("Search projects") diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml index 25d5dc92f8a..aaf1ab00eeb 100644 --- a/app/views/projects/merge_requests/dropdowns/_project.html.haml +++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml @@ -2,4 +2,4 @@ - projects.each do |project| %li %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } } - = project.path_with_namespace + = project.full_path diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index ea6cd16c7ad..d27e121beb4 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -17,6 +17,7 @@ -# haml-lint:disable InlineJavaScript :javascript + window.gl = window.gl || {}; window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} #js-vue-mr-widget.mr-widget diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 25109f0f414..e3bbebbcf4c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -17,8 +17,68 @@ - if import_sources_enabled? %p Create or Import your project from popular Git services - .col-lg-9 + .col-lg-9.js-toggle-container = form_for @project, html: { class: 'new_project' } do |f| + .create-project-options + .first-column + .project-template + .form-group + = f.label :template_project, class: 'label-light' do + Create from template + = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} + %div + = render 'project_templates', f: f + .second-column + - if import_sources_enabled? + .project-import + .form-group.clearfix + = f.label :visibility_level, class: 'label-light' do #the label here seems wrong + Import project from + .col-sm-12.import-buttons + %div + - if github_import_enabled? + = link_to new_import_github_path, class: 'btn import_github' do + = icon('github', text: 'GitHub') + %div + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? + = render 'bitbucket_import_modal' + %div + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? + = render 'gitlab_import_modal' + %div + - if google_code_import_enabled? + = link_to new_import_google_code_path, class: 'btn import_google_code' do + = icon('google', text: 'Google Code') + %div + - if fogbugz_import_enabled? + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_url, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div + - if git_import_enabled? + %button.btn.js-toggle-button.import_git{ type: "button" } + = icon('git', text: 'Repo by URL') + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = icon('gitlab', text: 'GitLab export') + + .row + .col-lg-12 + .js-toggle-content.hide + %hr + = render "shared/import_form", f: f + %hr + .row .form-group.col-xs-12.col-sm-6 = f.label :namespace_id, class: 'label-light' do @@ -45,53 +105,6 @@ Want to house several dependent projects under the same namespace? = link_to "Create a group", new_group_path - - if import_sources_enabled? - .project-import.js-toggle-container - .form-group.clearfix - = f.label :visibility_level, class: 'label-light' do - Import project from - .col-sm-12.import-buttons - %div - - if github_import_enabled? - = link_to new_import_github_path, class: 'btn import_github' do - = icon('github', text: 'GitHub') - %div - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do - = icon('bitbucket', text: 'Bitbucket') - - unless bitbucket_import_configured? - = render 'bitbucket_import_modal' - %div - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do - = icon('gitlab', text: 'GitLab.com') - - unless gitlab_import_configured? - = render 'gitlab_import_modal' - %div - - if google_code_import_enabled? - = link_to new_import_google_code_path, class: 'btn import_google_code' do - = icon('google', text: 'Google Code') - %div - - if fogbugz_import_enabled? - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'FogBugz') - %div - - if gitea_import_enabled? - = link_to new_import_gitea_url, class: 'btn import_gitea' do - = custom_icon('go_logo') - Gitea - %div - - if git_import_enabled? - %button.btn.js-toggle-button.import_git{ type: "button" } - = icon('git', text: 'Repo by URL') - .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - - if gitlab_project_import_enabled? - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - = icon('gitlab', text: 'GitLab export') - - .js-toggle-content.hide - = render "shared/import_form", f: f - .form-group = f.label :description, class: 'label-light' do Project description diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 7343d6e039c..bd8c38292d6 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -15,7 +15,7 @@ - else = s_("PipelineSchedules|None") %td.next-run-cell - - if pipeline_schedule.active? + - if pipeline_schedule.active? && pipeline_schedule.next_run_at = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else = s_("PipelineSchedules|Inactive") diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index 95706888655..78dc4817ed7 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -3,4 +3,4 @@ %h4 Runner ##{@runner.id} %hr - = render 'form', runner: @runner, runner_form_url: runner_path(@runner) + = render 'form', runner: @runner, runner_form_url: runner_path(@runner) diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index ef3599460f1..5dbcbf7eba6 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -39,7 +39,7 @@ Suggestions: %code= 'gitlab' %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace + %code= @project.full_path .form-group = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 73b99453a4b..c31c95608c6 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -33,7 +33,7 @@ Suggestions: %code= 'gitlab' %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace + %code= @project.full_path .form-group = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index adb8d5aaecb..e5a1fccf9ba 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -3,9 +3,12 @@ = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) - = f.hidden_field :title, value: @page.title - if @page.persisted? = f.hidden_field :last_commit_sha, value: @page.last_commit_sha + + .form-group + .col-sm-12= f.label :title, class: 'control-label-full-width' + .col-sm-12= f.text_field :title, class: 'form-control', value: @page.title .form-group .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index e71ce1f357f..f7283ae4739 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,21 +1,22 @@ %aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } } - .block.wiki-sidebar-header.append-bottom-default - %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } - = icon('angle-double-right') + .sidebar-container + .block.wiki-sidebar-header.append-bottom-default + %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } + = icon('angle-double-right') - - git_access_url = project_wikis_git_access_path(@project) - = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do - = succeed ' ' do - = icon('cloud-download') - Clone repository + - git_access_url = project_wikis_git_access_path(@project) + = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do + = succeed ' ' do + = icon('cloud-download') + Clone repository - .blocks-container - .block.block-first - %ul.wiki-pages - = render @sidebar_wiki_entries, context: 'sidebar' + .blocks-container + .block.block-first + %ul.wiki-pages + = render @sidebar_wiki_entries, context: 'sidebar' - .block - = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do - More Pages + .block + = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do + More Pages = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index e64dd6085fe..e740fb93ea4 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -7,7 +7,7 @@ .git-access-header Clone repository - %strong= @project_wiki.path_with_namespace + %strong= @project_wiki.full_path = render "shared/clone_panel", project: @project_wiki diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index b4843eafdb7..3d9c90c38fe 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -11,7 +11,7 @@ %span = default_clone_protocol.upcase = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown + %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown %li = ssh_clone_button(project) %li diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 1c7c73be933..873179339dc 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -1,16 +1,16 @@ .form-group.import-url-data - = f.label :import_url, class: 'control-label' do + = f.label :import_url, class: 'label-light' do %span Git repository URL - .col-sm-10 - = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' - .well.prepend-top-20 - %ul - %li - The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>. - %li - If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. - %li - The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. - %li - To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}. + = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + + .well.prepend-top-20 + %ul + %li + The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>. + %li + If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + %li + The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}. diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index 5f3cdaefd54..96502d7ce93 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,6 +1,7 @@ -- if @projects.any? - .project-item-select-holder +- if any_projects?(@projects) + .project-item-select-holder.btn-group.pull-right + %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label] } } + = icon('spinner spin') = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] - %a.btn.btn-new.new-project-item-select-button - = local_assigns[:label] + %button.btn.btn-new.new-project-item-select-button = icon('caret-down') diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml new file mode 100644 index 00000000000..eb5ddb0dde4 --- /dev/null +++ b/app/views/shared/_sidebar_toggle_button.html.haml @@ -0,0 +1,8 @@ +%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" } + = icon('angle-double-left') + = icon('angle-double-right') + %span.collapse-text Collapse sidebar + += button_tag class: 'close-nav-button', type: 'button' do + = icon ('times') + %span.collapse-text Close sidebar diff --git a/app/views/shared/icons/_abuse_reports.svg b/app/views/shared/icons/_abuse_reports.svg new file mode 100644 index 00000000000..fb16b269150 --- /dev/null +++ b/app/views/shared/icons/_abuse_reports.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg> diff --git a/app/views/shared/icons/_access_tokens.svg b/app/views/shared/icons/_access_tokens.svg new file mode 100644 index 00000000000..07ea6dab715 --- /dev/null +++ b/app/views/shared/icons/_access_tokens.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m13 2h-10c-1.7 0-3 1.3-3 3v6c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-6c0-1.7-1.3-3-3-3m1 9c0 .6-.4 1-1 1h-10c-.6 0-1-.4-1-1v-6c0-.6.4-1 1-1h10c.6 0 1 .4 1 1v6"/><circle cx="4" cy="8" r="1"/><circle cx="8" cy="8" r="1"/><circle cx="12" cy="8" r="1"/></svg> diff --git a/app/views/shared/icons/_account.svg b/app/views/shared/icons/_account.svg new file mode 100644 index 00000000000..d47e4f59914 --- /dev/null +++ b/app/views/shared/icons/_account.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m6.8 8c-.3 0-.5 0-.8 0-5 0-6 2.7-6 4.5s.1 2.5 6 2.5c.6 0 1.1 0 1.5 0-1-1.1-1.5-2.5-1.5-4 0-1.1.3-2.1.8-3"/><circle cx="6" cy="4" r="3"/><path d="m15.9 11.5l-.9-.6c0-.3-.1-.7-.2-.9l.6-.9c.1-.1.1-.2 0-.3l-.4-.5c-.1-.1-.2-.1-.3-.1l-.9.4c-.3-.2-.5-.3-.9-.4l-.3-1c0-.1-.1-.2-.2-.2h-.6c-.1 0-.2.1-.2.2l-.3 1c-.3.1-.6.2-.9.4l-1.1-.4c-.1 0-.2 0-.3.1l-.4.5c0 .1 0 .2 0 .3l.6.9c-.1.3-.2.6-.2.9l-.9.5c-.1.1-.1.2-.1.3l.1.6c0 .1.1.2.2.2l1.1.1c.1.2.3.4.5.6l-.2 1.2c0 .1 0 .2.1.3l.6.3c.1 0 .2 0 .3-.1l.9-.9c.2 0 .4 0 .6 0l.9.9c.1.1.2.1.3 0l.6-.3c.1 0 .2-.2.1-.3l-.1-1.1c.2-.2.4-.4.5-.6l1.1-.1c.1 0 .2-.1.2-.2l.1-.6c.1-.1.1-.2 0-.2m-3.9.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1"/></svg> diff --git a/app/views/shared/icons/_appearance.svg b/app/views/shared/icons/_appearance.svg new file mode 100644 index 00000000000..8ffeb780cb4 --- /dev/null +++ b/app/views/shared/icons/_appearance.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858-.411.022-.744.026-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023.172-.009.332-.02.478-.035-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg> diff --git a/app/views/shared/icons/_applications.svg b/app/views/shared/icons/_applications.svg new file mode 100644 index 00000000000..65442867174 --- /dev/null +++ b/app/views/shared/icons/_applications.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></svg> diff --git a/app/views/shared/icons/_authentication_log.svg b/app/views/shared/icons/_authentication_log.svg new file mode 100644 index 00000000000..0beb84c2912 --- /dev/null +++ b/app/views/shared/icons/_authentication_log.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></svg> diff --git a/app/views/shared/icons/_chat.svg b/app/views/shared/icons/_chat.svg new file mode 100644 index 00000000000..0c474c9f980 --- /dev/null +++ b/app/views/shared/icons/_chat.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.414 12l-3.707 3.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></svg> diff --git a/app/views/shared/icons/_container_registry.svg b/app/views/shared/icons/_container_registry.svg new file mode 100644 index 00000000000..56d62aab670 --- /dev/null +++ b/app/views/shared/icons/_container_registry.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m16 11.764v-8.764c0-1.657-1.343-3-3-3h-10c-1.657 0-3 1.343-3 3v8.764c.531-.475 1.232-.764 2-.764v-8c0-.552.448-1 1-1h10c.552 0 1 .448 1 1v8c.768 0 1.469.289 2 .764m-14 .236h12c1.105 0 2 .895 2 2 0 1.105-.895 2-2 2h-12c-1.105 0-2-.895-2-2 0-1.105.895-2 2-2m10 1c-.552 0-1 .448-1 1 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-.552-.448-1-1-1"/></svg> diff --git a/app/views/shared/icons/_doc_text.svg b/app/views/shared/icons/_doc_text.svg new file mode 100644 index 00000000000..92902a5b449 --- /dev/null +++ b/app/views/shared/icons/_doc_text.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></svg> diff --git a/app/views/shared/icons/_emails.svg b/app/views/shared/icons/_emails.svg new file mode 100644 index 00000000000..3ebc64bb03e --- /dev/null +++ b/app/views/shared/icons/_emails.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm0-2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3z"/><path d="M3.212 4L8 8.31 12.788 4H3.212zm6.126 5.796a2 2 0 0 1-2.676 0L.183 3.965A3.001 3.001 0 0 1 3 2h10c1.293 0 2.395.818 2.817 1.965l-6.48 5.83z"/></svg> diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg new file mode 100644 index 00000000000..439023c86d0 --- /dev/null +++ b/app/views/shared/icons/_issues.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></svg> diff --git a/app/views/shared/icons/_issues.svg.erb b/app/views/shared/icons/_issues.svg.erb deleted file mode 100644 index fa8655b5609..00000000000 --- a/app/views/shared/icons/_issues.svg.erb +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16" class="gitlab-icon"> - <path fill="#7E7C7C" d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2"></path> - <path fill="#7E7C7C" d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z"></path> -</svg> diff --git a/app/views/shared/icons/_java_spring.svg b/app/views/shared/icons/_java_spring.svg new file mode 100644 index 00000000000..508349aa456 --- /dev/null +++ b/app/views/shared/icons/_java_spring.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"> + <g fill="none" fill-rule="evenodd"> + <rect width="32" height="32"/> + <path fill="#70AD51" d="M5.46647617,27.9932117 C6.0517027,28.4658996 6.91159892,28.3777063 7.38425926,27.7914452 C7.85922261,27.2048452 7.76991326,26.3449044 7.18398981,25.8699411 C6.59874295,25.3956543 5.74015536,25.4869934 5.26383884,26.0722403 C4.81393367,26.6267596 4.87238621,27.4284565 5.37913494,27.9159868 L5.11431334,27.6818383 C1.97157151,24.7616933 0,20.5966301 0,15.9782542 C0,7.16842834 7.16775175,0 15.9796074,0 C20.4586065,0 24.5113565,1.8565519 27.4145869,4.8362365 C28.0749348,3.93840692 28.6466499,2.93435335 29.115524,1.82069284 C31.1513712,7.93770658 32.3482517,13.0811131 31.909824,17.1311567 C31.3178113,25.4044499 24.4017495,31.9585382 15.9796074,31.9585382 C12.0682639,31.9585382 8.48438805,30.5444735 5.7042963,28.2034861 L5.46647617,27.9932117 Z M29.0471888,23.0106888 C33.0546075,17.6737787 30.8211972,9.04527781 28.9612624,3.529749 C27.3029502,6.98304378 23.2217836,9.62375882 19.6981239,10.4613722 C16.3950312,11.2482417 13.4715032,10.6021021 10.4153644,11.7780085 C3.44517575,14.457289 3.55613585,22.7698242 7.39373146,24.6365249 C7.39711439,24.6392312 7.62444728,24.7616933 7.62174094,24.7576338 C7.62309411,24.7562806 13.2658211,23.6358542 16.3862356,22.4843049 C20.9450718,20.7996058 25.9524846,16.6494275 27.5986182,11.8273993 C26.723116,16.8415779 22.4179995,21.6669891 18.093262,23.8828081 C15.7908399,25.0648038 14.0005934,25.3279957 10.2123886,26.6385428 C9.74892722,26.798217 9.38492397,26.9538318 9.38492397,26.9538318 C10.3463526,26.7948341 11.301692,26.7420604 11.301692,26.7420604 C16.6954354,26.4869875 25.1087819,28.2582896 29.0471888,23.0106888 Z"/> + </g> +</svg> diff --git a/app/views/shared/icons/_key.svg b/app/views/shared/icons/_key.svg new file mode 100644 index 00000000000..5ad03ed4480 --- /dev/null +++ b/app/views/shared/icons/_key.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M7.574 6.689a4.002 4.002 0 0 1 6.275-4.861 4 4 0 0 1-4.86 6.275l-2.21 2.21.706.707a1 1 0 0 1-1.414 1.415l-.707-.708-.707.708.707.707a1 1 0 0 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.415-1.414l5.746-5.746zm2.033-.618a2 2 0 1 0 2.828-2.829 2 2 0 0 0-2.828 2.829z"/></svg> diff --git a/app/views/shared/icons/_key_2.svg b/app/views/shared/icons/_key_2.svg new file mode 100644 index 00000000000..368b2876c60 --- /dev/null +++ b/app/views/shared/icons/_key_2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></svg> diff --git a/app/views/shared/icons/_labels.svg b/app/views/shared/icons/_labels.svg new file mode 100644 index 00000000000..1ebad4bb4fa --- /dev/null +++ b/app/views/shared/icons/_labels.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667z"/><path d="M.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg> diff --git a/app/views/shared/icons/_lock.svg b/app/views/shared/icons/_lock.svg new file mode 100644 index 00000000000..703c09611a3 --- /dev/null +++ b/app/views/shared/icons/_lock.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m8 9c-.6 0-1 .4-1 1v1c0 .6.4 1 1 1s1-.4 1-1v-1c0-.6-.4-1-1-1"/><path d="m12 5v-1c0-2.2-1.8-4-4-4s-4 1.8-4 4v1c-1.7 0-3 1.3-3 3v5c0 1.7 1.3 3 3 3h8c1.7 0 3-1.3 3-3v-5c0-1.7-1.3-3-3-3m-6-1c0-1.1.9-2 2-2s2 .9 2 2v1h-4v-1m7 9c0 .6-.4 1-1 1h-8c-.6 0-1-.4-1-1v-5c0-.6.4-1 1-1h8c.6 0 1 .4 1 1v5"/></svg> diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg new file mode 100644 index 00000000000..68d957d6d11 --- /dev/null +++ b/app/views/shared/icons/_members.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></svg> diff --git a/app/views/shared/icons/_messages.svg b/app/views/shared/icons/_messages.svg new file mode 100644 index 00000000000..9a2ea15c35d --- /dev/null +++ b/app/views/shared/icons/_messages.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></svg> diff --git a/app/views/shared/icons/_monitoring.svg b/app/views/shared/icons/_monitoring.svg new file mode 100644 index 00000000000..21689b0877c --- /dev/null +++ b/app/views/shared/icons/_monitoring.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></svg> diff --git a/app/views/shared/icons/_node_express.svg b/app/views/shared/icons/_node_express.svg new file mode 100644 index 00000000000..f2c94319f19 --- /dev/null +++ b/app/views/shared/icons/_node_express.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"> + <g fill="none" fill-rule="evenodd" transform="translate(-3)"> + <rect width="32" height="32"/> + <path fill="#353535" d="M4.19170065,16.2667139 C4.23142421,18.3323387 4.47969269,20.2489714 4.93651356,22.0166696 C5.39333443,23.7843677 6.09841693,25.3236323 7.05178222,26.6345096 C8.00514751,27.9453869 9.23655921,28.9781838 10.7460543,29.7329313 C12.2555493,30.4876788 14.1026668,30.8650469 16.2874623,30.8650469 C19.5050701,30.8650469 22.1764391,30.0209341 24.3016492,28.3326831 C26.4268593,26.644432 27.7476477,24.1120935 28.2640539,20.7355914 L29.4557545,20.7355914 C29.0187954,24.3107112 27.6086304,27.0813875 25.2252172,29.0477034 C22.841804,31.0140194 19.9023051,31.9971626 16.4066324,31.9971626 C14.0232191,32.0368861 11.9874175,31.659518 10.2991665,30.8650469 C8.61091547,30.0705759 7.23054269,28.9484023 6.15800673,27.4984926 C5.08547078,26.0485829 4.29101162,24.3404957 3.77460543,22.3741798 C3.25819923,20.4078639 3,18.2926164 3,16.0283738 C3,13.4860664 3.3773681,11.2218578 4.13211562,9.23568007 C4.88686314,7.24950238 5.87993709,5.57120741 7.11136726,4.20074481 C8.34279742,2.8302822 9.77282391,1.78755456 11.4014896,1.07253059 C13.0301553,0.357506621 14.6985195,0 16.4066324,0 C18.7900456,0 20.8457087,0.456814016 22.5736832,1.37045575 C24.3016578,2.28409749 25.7118228,3.4956477 26.8042206,5.00514275 C27.8966183,6.51463779 28.6910775,8.24258646 29.1876219,10.1890406 C29.6841663,12.1354947 29.8927118,14.1613656 29.8132647,16.2667139 L4.19170065,16.2667139 Z M28.6215641,15.0750133 C28.6215641,13.2080062 28.3633648,11.4304039 27.8469586,9.74215285 C27.3305524,8.05390181 26.5658855,6.57422163 25.5529349,5.30306791 C24.5399843,4.03191419 23.2787803,3.0289095 21.7692853,2.29402376 C20.2597903,1.55913801 18.5119801,1.19170065 16.5258024,1.19170065 C14.8574132,1.19170065 13.2982871,1.50948432 11.8483774,2.14506118 C10.3984676,2.78063804 9.12733299,3.70419681 8.03493526,4.9157652 C6.94253754,6.12733359 6.05870172,7.58715229 5.38340131,9.2952651 C4.70810089,11.0033779 4.31087132,12.9299414 4.19170065,15.0750133 L28.6215641,15.0750133 Z"/> + </g> +</svg> diff --git a/app/views/shared/icons/_notifications.svg b/app/views/shared/icons/_notifications.svg new file mode 100644 index 00000000000..da55de041da --- /dev/null +++ b/app/views/shared/icons/_notifications.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></svg> diff --git a/app/views/shared/icons/_overview.svg b/app/views/shared/icons/_overview.svg new file mode 100644 index 00000000000..4791282df7f --- /dev/null +++ b/app/views/shared/icons/_overview.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2 2v3h3V2H2zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm9 2v3h3V2h-3zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zM2 11v3h3v-3H2zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm9 2v3h3v-3h-3zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z"/></svg> diff --git a/app/views/shared/icons/_pipeline.svg b/app/views/shared/icons/_pipeline.svg new file mode 100644 index 00000000000..5bedc96a1bd --- /dev/null +++ b/app/views/shared/icons/_pipeline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m8 0c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8m0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6"/><circle cx="12.5" cy="9.5" r=".5"/><circle cx="12.5" cy="6.5" r=".5"/><circle cx="10.5" cy="12.5" r=".5"/><circle cx="10.5" cy="3.5" r=".5"/><circle cx="5.5" cy="12.5" r=".5"/><circle cx="5.5" cy="3.5" r=".5"/><circle cx="3.5" cy="9.5" r=".5"/><circle cx="3.5" cy="6.5" r=".5"/><path d="m9 7.2c0 0 0-.1 0-.2v-1.9c0-.1 0-.1-.1-.2l-.8-.8c0 0-.1 0-.1 0l-.9.8c-.1.1-.1.1-.1.2v1.9c0 .1 0 .2 0 .2-.6.4-1 1-1 1.8 0 1.1.9 2 2 2s2-.9 2-2c0-.8-.4-1.4-1-1.8m-1 2.8c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1"/></svg> diff --git a/app/views/shared/icons/_preferences.svg b/app/views/shared/icons/_preferences.svg new file mode 100644 index 00000000000..cbd7a4fe9f0 --- /dev/null +++ b/app/views/shared/icons/_preferences.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2zm10 5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zm-5 5h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2z" fill-rule="evenodd"/></svg> diff --git a/app/views/shared/icons/_profile.svg b/app/views/shared/icons/_profile.svg new file mode 100644 index 00000000000..29e360a9051 --- /dev/null +++ b/app/views/shared/icons/_profile.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg> diff --git a/app/views/shared/icons/_project.svg b/app/views/shared/icons/_project.svg new file mode 100644 index 00000000000..bbfdd939e7b --- /dev/null +++ b/app/views/shared/icons/_project.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></svg> diff --git a/app/views/shared/icons/_project.svg.erb b/app/views/shared/icons/_project.svg.erb deleted file mode 100644 index 2f60bb7245e..00000000000 --- a/app/views/shared/icons/_project.svg.erb +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"> - <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E" fill-rule="evenodd"></path> -</svg> diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg new file mode 100644 index 00000000000..0bb09a705df --- /dev/null +++ b/app/views/shared/icons/_rails.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"> + <g fill="none" fill-rule="evenodd" transform="translate(0 -6)"> + <rect width="32" height="32"/> + <path fill="#C00" fill-rule="nonzero" d="M0.984615385,25.636044 C0.984615385,25.636044 1.40659341,21.4725275 4.36043956,16.5494505 C7.31428571,11.6263736 12.3498901,7.8989011 16.4430769,7.53318681 C24.5872527,6.71736264 31.9015385,14.0175824 31.9015385,14.0175824 C31.9015385,14.0175824 31.6624176,14.1863736 31.4092308,14.3973626 C23.4197802,8.48967033 18.5389011,11.2747253 17.0057143,12.0202198 C9.97274725,15.9446154 12.0967033,25.636044 12.0967033,25.636044 L0.984615385,25.636044 Z M24.1371429,8.32087912 C23.687033,8.13802198 23.2369231,7.96923077 22.7727473,7.81450549 L22.829011,6.88615385 C23.7151648,7.13934066 24.0668132,7.30813187 24.1934066,7.37846154 L24.1371429,8.32087912 Z M22.8008791,11.3028571 C23.250989,11.330989 23.7151648,11.3872527 24.1934066,11.4857143 L24.1371429,12.3578022 C23.672967,12.2593407 23.2087912,12.2030769 22.7446154,12.189011 L22.8008791,11.3028571 Z M17.5964835,6.91428571 C17.1885714,6.91428571 16.7806593,6.92835165 16.3727473,6.97054945 L16.1054945,6.14065934 C16.5696703,6.0843956 17.0197802,6.05626374 17.4558242,6.05626374 L17.7371429,6.91428571 C17.6949451,6.91428571 17.6386813,6.91428571 17.5964835,6.91428571 Z M18.2716484,12.0905495 C18.6232967,11.9358242 19.0312088,11.7810989 19.5094505,11.6404396 L19.8189011,12.5687912 C19.410989,12.6953846 19.0030769,12.8641758 18.5951648,13.0610989 L18.2716484,12.0905495 Z M11.8857143,8.39120879 C11.52,8.57406593 11.1683516,8.78505495 10.8026374,9.01010989 L10.1556044,8.02549451 C10.5353846,7.80043956 10.9010989,7.60351648 11.2527473,7.42065934 L11.8857143,8.39120879 Z M14.7692308,14.7208791 C15.0224176,14.3973626 15.3178022,14.0738462 15.6413187,13.7784615 L16.2742857,14.7349451 C15.9648352,15.0584615 15.6835165,15.381978 15.4443956,15.7336264 L14.7692308,14.7208791 Z M12.7296703,19.2501099 C12.8421978,18.7437363 12.9687912,18.2232967 13.1516484,17.7028571 L14.1643956,18.5046154 C14.0237363,19.0531868 13.9252747,19.6017582 13.869011,20.1503297 L12.7296703,19.2501099 Z M6.56879121,12.5687912 C6.23120879,12.9204396 5.90769231,13.3002198 5.61230769,13.68 L4.52923077,12.7516484 C4.85274725,12.4 5.2043956,12.0483516 5.57010989,11.6967033 L6.56879121,12.5687912 Z M2.32087912,18.8562637 C2.09582418,19.3767033 1.80043956,20.0659341 1.61758242,20.5441758 L0,19.9534066 C0.140659341,19.5736264 0.436043956,18.8703297 0.703296703,18.2654945 L2.32087912,18.8562637 Z M12.5186813,22.8228571 L14.0378022,23.3714286 C14.1221978,24.0325275 14.2487912,24.6514286 14.3753846,25.2 L12.6874725,24.5951648 C12.6171429,24.1731868 12.5468132,23.5683516 12.5186813,22.8228571 Z"/> + </g> +</svg> diff --git a/app/views/shared/icons/_service_templates.svg b/app/views/shared/icons/_service_templates.svg new file mode 100644 index 00000000000..b65cd8300b2 --- /dev/null +++ b/app/views/shared/icons/_service_templates.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></svg> diff --git a/app/views/shared/icons/_settings.svg b/app/views/shared/icons/_settings.svg new file mode 100644 index 00000000000..96c5ef8c04d --- /dev/null +++ b/app/views/shared/icons/_settings.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918c.594.181 1.15.452 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg> diff --git a/app/views/shared/icons/_snippets.svg b/app/views/shared/icons/_snippets.svg new file mode 100644 index 00000000000..1e1340187b4 --- /dev/null +++ b/app/views/shared/icons/_snippets.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 1 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 1 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></svg> diff --git a/app/views/shared/icons/_spam_logs.svg b/app/views/shared/icons/_spam_logs.svg new file mode 100644 index 00000000000..80ee0eb3856 --- /dev/null +++ b/app/views/shared/icons/_spam_logs.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg> diff --git a/app/views/shared/icons/_system_hooks.svg b/app/views/shared/icons/_system_hooks.svg new file mode 100644 index 00000000000..7b95a6f29f3 --- /dev/null +++ b/app/views/shared/icons/_system_hooks.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></svg> diff --git a/app/views/shared/icons/_wiki.svg b/app/views/shared/icons/_wiki.svg new file mode 100644 index 00000000000..b5ad38d9863 --- /dev/null +++ b/app/views/shared/icons/_wiki.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 8 6.191V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></svg> diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 3428d6e0445..1ad00461d76 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,5 +1,6 @@ - type = local_assigns.fetch(:type) - block_css_class = type != :boards_modal ? 'row-content-block second-block' : '' +- full_path = @project.present? ? @project.full_path : @group.full_path .issues-filters .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal } @@ -18,7 +19,7 @@ dropdown_class: "filtered-search-history-dropdown", content_class: "filtered-search-history-dropdown-content", title: "Recent searches" }) do - .js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } } + .js-filtered-search-history-dropdown{ data: { full_path: full_path } } .filtered-search-box-input-container.droplab-dropdown .scroll-container %ul.tokens-container.list-unstyled diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 895fb8247b5..66ac8196f2f 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -67,7 +67,7 @@ .block.issues .sidebar-collapsed-icon %strong - = icon('hashtag', 'aria-hidden': 'true') + = custom_icon('issues') %span= milestone.issues_visible_to_user(current_user).count .title.hide-collapsed Issues diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 7ed6c622558..914506bf0ce 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -10,7 +10,7 @@ - load_pipeline_status(projects) .js-projects-list-holder - - if projects.any? + - if any_projects?(projects) %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil @@ -22,7 +22,7 @@ %li.project-row.private-forks-notice = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') %strong= pluralize(@private_forks_count, 'private fork') - %span you have no access to. + %span you have no access to. = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages - else .nothing-here-block No projects found diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 00788e77b6b..093b2d82813 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -37,7 +37,3 @@ .col-md-3 = hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" = submit_tag "Register U2F device", class: "btn btn-success" - -:javascript - var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f); - u2fRegister.start(); diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 805a346a85e..6b1d75c6e72 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,6 +1,6 @@ %h4.prepend-top-20 Contributions for - %strong= @calendar_date.to_s(:short) + %strong= @calendar_date.to_s(:medium) - if @events.any? %ul.bordered-list @@ -8,7 +8,7 @@ %li %span.light %i.fa.fa-clock-o - = event.created_at.to_s(:time) + = event.created_at.strftime('%-I:%M%P') - if event.push? #{event.action_name} #{event.ref_type} %strong @@ -30,4 +30,4 @@ = event.project_name - else %p - No contributions found for #{@calendar_date.to_s(:short)} + No contributions found for #{@calendar_date.to_s(:medium)} diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index a449706c567..879e0f99b14 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -104,7 +104,7 @@ .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path } } + .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index d3f7e479a8d..1afa24c8e2a 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -31,8 +31,6 @@ class EmailReceiverWorker when Gitlab::Email::EmptyEmailError can_retry = true "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." - when Gitlab::Email::AutoGeneratedEmailError - "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." when Gitlab::Email::UserNotFoundError "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." when Gitlab::Email::UserBlockedError diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 22f67fa9e9f..3dd14466994 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -66,7 +66,7 @@ class IrkerWorker end def send_new_branch(project, repo_name, committer, branch) - repo_path = project.path_with_namespace + repo_path = project.full_path newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches" newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors @@ -109,7 +109,7 @@ class IrkerWorker end def send_commits_count(data, project, repo, committer, branch) - url = compare_url data, project.path_with_namespace + url = compare_url data, project.full_path commits = colorize_commits data['total_commits_count'] new_commits = 'new commit' diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 625476b7e01..6be541abd3e 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -16,7 +16,7 @@ class RepositoryImportWorker Gitlab::Metrics.add_event(:import_repository, import_url: @project.import_url, - path: @project.path_with_namespace) + path: @project.full_path) project.update_columns(import_jid: self.jid, import_error: nil) |