diff options
Diffstat (limited to 'app')
351 files changed, 4360 insertions, 1964 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index d5e11e22be5..f4f8cf04184 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -21,16 +21,14 @@ }; Activities.prototype.toggleFilter = function(sender) { - var event_filters, filter; + var filter = sender.attr("id").split("_")[0]; + $('.event-filter .active').removeClass("active"); - event_filters = $.cookie("event_filter"); - filter = sender.attr("id").split("_")[0]; - $.cookie("event_filter", (event_filters !== filter ? filter : ""), { + $.cookie("event_filter", filter, { path: gon.relative_url_root || '/' }); - if (event_filters !== filter) { - return sender.closest('li').toggleClass("active"); - } + + sender.closest('li').toggleClass("active"); }; return Activities; diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 599331df3f5..56ec1489f89 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -6,11 +6,10 @@ groupProjectsPath: "/api/:version/groups/:id/projects.json", projectsPath: "/api/:version/projects.json?simple=true", labelsPath: "/:namespace_path/:project_path/labels", - licensePath: "/api/:version/licenses/:key", - gitignorePath: "/api/:version/gitignores/:key", - gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", + licensePath: "/api/:version/templates/licenses/:key", + gitignorePath: "/api/:version/templates/gitignores/:key", + gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", - group: function(group_id, callback) { var url = Api.buildUrl(Api.groupPath) .replace(':id', group_id); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 91c12570e09..d4f8f4b9420 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -28,12 +28,13 @@ $(() => { state: Store.state, loading: true, endpoint: $boardApp.dataset.endpoint, + boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase }, init: Store.create.bind(Store), created () { - gl.boardService = new BoardService(this.endpoint); + gl.boardService = new BoardService(this.endpoint, this.boardId); }, ready () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 7e86f001f44..cacb36a897f 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,7 +21,8 @@ }, data () { return { - filters: Store.state.filters + filters: Store.state.filters, + showIssueForm: false }; }, watch: { @@ -33,6 +34,11 @@ deep: true } }, + methods: { + showNewIssueForm() { + this.showIssueForm = !this.showIssueForm; + } + }, ready () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 index 63d72d857d9..ff90f2d6d75 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -8,10 +8,8 @@ data () { return { predefinedLabels: [ - new ListLabel({ title: 'Development', color: '#5CB85C' }), - new ListLabel({ title: 'Testing', color: '#F0AD4E' }), - new ListLabel({ title: 'Production', color: '#FF5F00' }), - new ListLabel({ title: 'Ready', color: '#FF0000' }) + new ListLabel({ title: 'To Do', color: '#F0AD4E' }), + new ListLabel({ title: 'Doing', color: '#5CB85C' }) ] } }, diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 474805c1437..7022a29e818 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,5 @@ //= require ./board_card +//= require ./board_new_issue (() => { const Store = gl.issueBoards.BoardsStore; @@ -8,14 +9,16 @@ gl.issueBoards.BoardList = Vue.extend({ components: { - 'board-card': gl.issueBoards.BoardCard + 'board-card': gl.issueBoards.BoardCard, + 'board-new-issue': gl.issueBoards.BoardNewIssue }, props: { disabled: Boolean, list: Object, issues: Array, loading: Boolean, - issueLinkBase: String + issueLinkBase: String, + showIssueForm: Boolean }, data () { return { @@ -73,7 +76,7 @@ group: 'issues', sort: false, disabled: this.disabled, - filter: '.board-list-count', + filter: '.board-list-count, .is-disabled', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 new file mode 100644 index 00000000000..a4fad422eca --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -0,0 +1,58 @@ +(() => { + window.gl = window.gl || {}; + + gl.issueBoards.BoardNewIssue = Vue.extend({ + props: { + list: Object, + showIssueForm: Boolean + }, + data() { + return { + title: '', + error: false + }; + }, + watch: { + showIssueForm () { + this.$els.input.focus(); + } + }, + methods: { + submit(e) { + e.preventDefault(); + if (this.title.trim() === '') return; + + this.error = false; + + const labels = this.list.label ? [this.list.label] : []; + const issue = new ListIssue({ + title: this.title, + labels + }); + + this.list.newIssue(issue) + .then((data) => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$els.submitButton).enable(); + }) + .catch(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$els.submitButton).enable(); + + // Remove the issue + this.list.removeIssue(issue); + + // Show error message + this.error = true; + this.showIssueForm = true; + }); + + this.cancel(); + }, + cancel() { + this.showIssueForm = false; + this.title = ''; + } + } + }); +})(); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index 44addb3ea98..f629d45c587 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -21,7 +21,7 @@ fallbackClass: 'is-dragging', fallbackOnBody: true, ghostClass: 'is-ghost', - filter: '.has-tooltip', + filter: '.has-tooltip, .btn', delay: gl.issueBoards.touchEnabled ? 100 : 0, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 91fd620fdb3..5d0a561cdba 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -87,6 +87,17 @@ class List { }); } + newIssue (issue) { + this.addIssue(issue); + this.issuesSize++; + + return gl.boardService.newIssue(this.id, issue) + .then((resp) => { + const data = resp.json(); + issue.id = data.iid; + }); + } + createIssues (data) { data.forEach((issueObj) => { this.addIssue(new ListIssue(issueObj)); diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 9b80fb2e99f..b9c91cbf31e 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,15 +1,15 @@ class BoardService { - constructor (root) { + constructor (root, boardId) { Vue.http.options.root = root; - this.lists = Vue.resource(`${root}/lists{/id}`, {}, { + this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { generate: { method: 'POST', - url: `${root}/lists/generate.json` + url: `${root}/${boardId}/lists/generate.json` } }); - this.issue = Vue.resource(`${root}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/lists{/id}/issues`, {}); + this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); Vue.http.interceptors.push((request, next) => { request.headers['X-CSRF-Token'] = $.rails.csrfToken(); @@ -58,4 +58,10 @@ class BoardService { to_list_id }); } + + newIssue (id, issue) { + return this.issues.save({ id }, { + issue + }); + } }; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index f336bfc36d6..97462a5959c 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -15,18 +15,17 @@ this.hideSidebar = bind(this.hideSidebar, this); this.toggleSidebar = bind(this.toggleSidebar, this); this.updateDropdown = bind(this.updateDropdown, this); + this.$document = $(document); clearInterval(Build.interval); // Init breakpoint checker this.bp = Breakpoints.get(); - $('.js-build-sidebar').niceScroll(); + this.initSidebar(); this.populateJobs(this.build_stage); this.updateStageDropdownText(this.build_stage); - this.hideSidebar(); - $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); $(window).off('resize.build').on('resize.build', this.hideSidebar); - $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); + this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); $('#js-build-scroll > a').off('click').on('click', this.stepTrace); this.updateArtifactRemoveDate(); if ($('#build-trace').length) { @@ -62,6 +61,21 @@ } } + Build.prototype.initSidebar = function() { + this.$sidebar = $('.js-build-sidebar'); + this.sidebarTranslationLimits = { + min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + } + this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight(); + this.$sidebar.css({ + top: this.sidebarTranslationLimits.max + }); + this.$sidebar.niceScroll(); + this.hideSidebar(); + this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); + this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this)); + }; + Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] @@ -129,15 +143,23 @@ Build.prototype.toggleSidebar = function() { if (this.shouldHideSidebar()) { - return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed'); + return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed'); } }; + Build.prototype.translateSidebar = function(e) { + var newPosition = this.sidebarTranslationLimits.max - document.body.scrollTop; + if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min; + this.$sidebar.css({ + top: newPosition + }); + }; + Build.prototype.hideSidebar = function() { if (this.shouldHideSidebar()) { - return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); } else { - return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); } }; diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image_file.js index e893491b19b..e893491b19b 100644 --- a/app/assets/javascripts/commit/image-file.js +++ b/app/assets/javascripts/commit/image_file.js diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js.es6 index 294d2c9052c..9a2082d97e0 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -9,7 +9,10 @@ var $dropdown, selected; $dropdown = $(this); selected = $dropdown.data('selected'); - return $dropdown.glDropdown({ + const $dropdownContainer = $dropdown.closest('.dropdown'); + const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); + const $filterInput = $('input[type="search"]', $dropdownContainer); + $dropdown.glDropdown({ data: function(term, callback) { return $.ajax({ url: $dropdown.data('refs-url'), @@ -42,6 +45,14 @@ return $el.text().trim(); } }); + $filterInput.on('keyup', (e) => { + const keyCode = e.keyCode || e.which; + if (keyCode !== 13) return; + const text = $filterInput.val(); + $fieldInput.val(text); + $('.dropdown-toggle-text', $dropdown).text(text); + $dropdownContainer.removeClass('open'); + }); }); }; diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index cd9886ba58d..cd9886ba58d 100644 --- a/app/assets/javascripts/cycle-analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js.es6 index 8d99b12102d..f3957ed374b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -8,6 +8,7 @@ Dispatcher = (function() { function Dispatcher() { this.initSearch(); + this.initFieldErrors(); this.initPageScripts(); } @@ -20,7 +21,11 @@ path = page.split(':'); shortcut_handler = null; switch (page) { + case 'sessions:new': + new UsernameValidator(); + break; case 'projects:boards:show': + case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); break; case 'projects:merge_requests:index': @@ -126,6 +131,9 @@ new TreeView(); } break; + case 'projects:pipelines:show': + new gl.Pipelines(); + break; case 'groups:activity': new Activities(); break; @@ -136,12 +144,12 @@ break; case 'groups:group_members:index': new gl.MemberExpirationDate(); - new GroupMembers(); + new gl.Members(); new UsersSelect(); break; case 'projects:project_members:index': new gl.MemberExpirationDate(); - new ProjectMembers(); + new gl.Members(); new UsersSelect(); break; case 'groups:new': @@ -287,6 +295,12 @@ } }; + Dispatcher.prototype.initFieldErrors = function() { + $('.show-gl-field-errors').each((i, form) => { + new gl.GlFieldErrors(form); + }); + }; + return Dispatcher; })(); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index d0786bf0053..845313b6b38 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -52,37 +52,27 @@ } } }, - setup: function(input) { + setup: _.debounce(function(input) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); // destroy previous instances this.destroyAtWho(); // set up instances this.setupAtWho(); - if (this.dataSource) { - if (!this.dataLoading && !this.cachedData) { - this.dataLoading = true; - setTimeout((function(_this) { - return function() { - var fetch; - fetch = _this.fetchData(_this.dataSource); - return fetch.done(function(data) { - _this.dataLoading = false; - return _this.loadData(data); - }); - }; - // We should wait until initializations are done - // and only trigger the last .setup since - // The previous .dataSource belongs to the previous issuable - // and the last one will have the **proper** .dataSource property - // TODO: Make this a singleton and turn off events when moving to another page - })(this), 1000); - } - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } + + if (this.dataSource && !this.dataLoading && !this.cachedData) { + this.dataLoading = true; + return this.fetchData(this.dataSource) + .done((data) => { + this.dataLoading = false; + this.loadData(data); + }); + }; + + if (this.cachedData != null) { + return this.loadData(this.cachedData); } - }, + }, 1000), setupAtWho: function() { // Emoji this.input.atwho({ diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d4403375643..53762f2965c 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -25,7 +25,7 @@ return function(e) { e.preventDefault(); e.stopPropagation(); - return _this.input.val('').trigger('keyup').focus(); + return _this.input.val('').trigger('input').focus(); }; })(this)); // Key events @@ -37,28 +37,16 @@ e.preventDefault() } }) - .on('keyup', function(e) { - var keyCode; - keyCode = e.which; - if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) { - return; - } + .on('input', function() { if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.addClass(HAS_VALUE_CLASS); } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.removeClass(HAS_VALUE_CLASS); } - if (keyCode === 13 && !options.elIsInput) { - return false; - } // Only filter asynchronously only if option remote is set if (this.options.remote) { clearTimeout(timeout); return timeout = setTimeout(function() { - var blurField = this.shouldBlur(keyCode); - if (blurField && this.filterInputBlur) { - this.input.blur(); - } return this.options.query(this.input.val(), function(data) { return this.options.callback(data); }.bind(this)); @@ -255,7 +243,7 @@ _this.fullData = data; _this.parseData(_this.fullData); if (_this.options.filterable && _this.filter && _this.filter.input) { - return _this.filter.input.trigger('keyup'); + return _this.filter.input.trigger('input'); } }; // Remote data @@ -487,7 +475,7 @@ // Triggering 'keyup' will re-render the dropdown which is not always required // specially if we want to keep the state of the dropdown needed for bulk-assignment if (!this.options.persistWhenHide) { - $input.trigger("keyup"); + $input.trigger("input"); } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); @@ -500,14 +488,27 @@ // Render the full menu GitLabDropdown.prototype.renderMenu = function(html) { - var menu_html; - menu_html = ""; if (this.options.renderMenu) { - menu_html = this.options.renderMenu(html); + return this.options.renderMenu(html); } else { - menu_html = $('<ul />').append(html); + var ul = document.createElement('ul'); + + for (var i = 0; i < html.length; i++) { + var el = html[i]; + + if (el instanceof jQuery) { + el = el.get(0); + } + + if (typeof el === 'string') { + ul.innerHTML += el; + } else { + ul.appendChild(el); + } + } + + return ul; } - return menu_html; }; // Append the menu into the dropdown @@ -521,7 +522,7 @@ }; GitLabDropdown.prototype.renderItem = function(data, group, index) { - var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value; + var field, fieldName, html, selected, text, url, value; if (group == null) { group = false; } @@ -529,18 +530,16 @@ // Render the row index = false; } - html = ""; - // Divider - if (data === "divider") { - return "<li class='divider'></li>"; - } - // Separator is a full-width divider - if (data === "separator") { - return "<li class='separator'></li>"; + html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { + html.className = data; + return html; } // Header if (data.header != null) { - return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header }); + html.className = 'dropdown-header'; + html.innerHTML = data.header; + return html; } if (this.options.renderRow) { // Call the render function @@ -567,24 +566,25 @@ } else { text = data.text != null ? data.text : ''; } - cssClass = ""; - if (selected) { - cssClass = "is-active"; - } if (this.highlight) { text = this.highlightTextMatches(text, this.filterInput.val()); } + // Create the list item & the link + var link = document.createElement('a'); + + link.href = url; + link.innerHTML = text; + + if (selected) { + link.className = 'is-active'; + } + if (group) { - groupAttrs = 'data-group=' + group + ' data-index=' + index; - } else { - groupAttrs = ''; + link.dataset.group = group; + link.dataset.index = index; } - html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({ - url: url, - groupAttrs: groupAttrs, - cssClass: cssClass, - text: text - }); + + html.appendChild(link); } return html; }; @@ -738,6 +738,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { + e.preventDefault(); _this.selectRowAtIndex(); } }; diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 new file mode 100644 index 00000000000..8657e7b4abf --- /dev/null +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -0,0 +1,167 @@ +((global) => { + /* + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `show-gl-field-errors` to the form element, and ensure error messages are + * declared in each inputs' title attribute. + * + * Example: + * + * <form class='show-gl-field-errors'> + * <input type='text' name='username' title='Username is required.'/> + *</form> + * + * */ + + const errorMessageClass = 'gl-field-error'; + const inputErrorClass = 'gl-field-error-outline'; + + class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`); + + this.state = { + valid: false, + empty: true + }; + + this.initFieldValidation(); + } + + initFieldValidation() { + // hidden when injected into DOM + this.inputElement.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } + + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity + const ignoreSelector = '.validation-ignore'; + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); + + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + if (this.state.valid) { + return this.renderValid(); + } + + if (this.state.empty) { + return this.renderEmpty(); + } + + if (!this.state.valid) { + return this.renderInvalid(); + } + + } + + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.field_validator') + .on('keyup.field_validator', this.updateValidity.bind(this)); + + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } + + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); + } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); + } + } + + const customValidationFlag = 'no-gl-field-errors'; + + class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } + + initValidators () { + // select all non-hidden inputs in form + this.state.inputs = this.form.find(':input:not([type=hidden])').toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new GlFieldError({ input, formErrors: this })); + + this.form.on('submit', this.catchInvalidFormSubmit); + } + + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ + + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + } + + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); + } + } + + global.GlFieldErrors = GlFieldErrors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js deleted file mode 100644 index 4382dd6860f..00000000000 --- a/app/assets/javascripts/groups.js +++ /dev/null @@ -1,13 +0,0 @@ -(function() { - this.GroupMembers = (function() { - function GroupMembers() { - $('li.group_member').bind('ajax:success', function() { - return $(this).fadeOut(); - }); - } - - return GroupMembers; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/issues-bulk-assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 0808f538f01..0808f538f01 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 diff --git a/app/assets/javascripts/LabelManager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index bc68e53504f..bc68e53504f 100644 --- a/app/assets/javascripts/LabelManager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index e356872624a..f1e719937c7 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -292,7 +292,7 @@ return; } - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { return; } if ($dropdown.hasClass('js-multiselect')) { @@ -334,7 +334,7 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 9299d0eabd2..b170e26eebf 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -38,6 +38,11 @@ gl.utils.getPagePath = function() { return $('body').data('page').split(':')[0]; }; + gl.utils.parseUrl = function (url) { + var parser = document.createElement('a'); + parser.href = url; + return parser; + }; return jQuery.timefor = function(time, suffix, expiredLabel) { var suffixFromNow, timefor; if (!time) { diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index 1935af491f7..e1532fd9ec4 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -14,14 +14,18 @@ inputs.datepicker({ dateFormat: 'yy-mm-dd', minDate: 1, - onSelect: toggleClearInput + onSelect: function () { + $(this).trigger('change'); + toggleClearInput.call(this); + } }); inputs.next('.js-clear-input').on('click', function(event) { event.preventDefault(); var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); - input.datepicker('setDate', null); + input.datepicker('setDate', null) + .trigger('change'); toggleClearInput.call(input); }); diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 new file mode 100644 index 00000000000..a0cd20f21e8 --- /dev/null +++ b/app/assets/javascripts/members.js.es6 @@ -0,0 +1,36 @@ +((w) => { + w.gl = w.gl || {}; + + class Members { + constructor() { + this.addListeners(); + } + + addListeners() { + $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); + $('.js-member-update-control').off('change').on('change', this.formSubmit); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); + } + + removeRow(e) { + const $target = $(e.target); + + if ($target.hasClass('btn-remove')) { + $target.closest('.member') + .fadeOut(function () { + $(this).remove(); + }); + } + } + + formSubmit() { + $(this).closest('form').trigger("submit.rails").end().disable(); + } + + formSuccess() { + $(this).find('.js-member-update-control').enable(); + } + } + + gl.Members = Members; +})(window); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index bec11a198a1..fd21aa1fefa 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -61,6 +61,9 @@ function MergeRequestTabs(opts) { this.opts = opts != null ? opts : {}; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; + + this.buildsLoaded = this.opts.buildsLoaded || false; + this.setCurrentAction = bind(this.setCurrentAction, this); this.tabShown = bind(this.tabShown, this); this.showTab = bind(this.showTab, this); @@ -68,6 +71,7 @@ this._location = location; this.bindEvents(); this.activateTab(this.opts.action); + this.initAffix(); } MergeRequestTabs.prototype.bindEvents = function() { @@ -93,7 +97,7 @@ this.loadCommits($target.attr('href')); this.expandView(); this.resetViewContainer(); - } else if (action === 'diffs') { + } else if (this.isDiffAction(action)) { this.loadDiff($target.attr('href')); if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { this.shrinkView(); @@ -170,8 +174,9 @@ action = 'notes'; } this.currentAction = action; - // Remove a trailing '/commits' or '/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); + // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' + new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + // Append the new action if we're on a tab other than 'notes' if (action !== 'notes') { new_state += "/" + action; @@ -210,8 +215,13 @@ if (this.diffsLoaded) { return; } + + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + var url = gl.utils.parseUrl(source); + return this._get({ - url: (source + ".json") + this._location.search, + url: (url.pathname + ".json") + this._location.search, success: (function(_this) { return function(data) { $('#diffs').html(data.html); @@ -223,7 +233,7 @@ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); $('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') { + if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { _this.expandViewContainer(); } _this.diffsLoaded = true; @@ -324,6 +334,10 @@ return $('.inline-parallel-buttons a.active').data('view-type'); }; + MergeRequestTabs.prototype.isDiffAction = function(action) { + return action === 'diffs' || action === 'new/diffs' + }; + MergeRequestTabs.prototype.expandViewContainer = function() { var $wrapper = $('.content-wrapper .container-fluid'); if (this.fixedLayoutPref === null) { @@ -367,6 +381,46 @@ // Only when sidebar is collapsed }; + MergeRequestTabs.prototype.initAffix = function () { + var $tabs = $('.js-tabs-affix'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + + var tabsWidth = $tabs.outerWidth(), + $diffTabs = $('#diff-notes-app'), + offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height()); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: offsetTop + } + }).on('affix.bs.affix', function () { + $tabs.css({ + left: $tabs.offset().left, + width: tabsWidth + }); + $diffTabs.css({ + marginTop: $tabs.height() + }); + }).on('affix-top.bs.affix', function () { + $tabs.css({ + left: '', + width: '' + }); + $diffTabs.css({ + marginTop: '' + }); + }); + + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); + } + }; + return MergeRequestTabs; })(); diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js.es6 index 7bbcdf59838..fcadc4bc515 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,7 +1,26 @@ -(function() { + ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - this.MergeRequestWidget = (function() { + const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> + <div class="ci_widget ci-success"> + <%= ci_success_icon %> + <span> + Deployed to + <a href="<%- url %>" target="_blank" class="environment"> + <%- name %> + </a> + <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> + <%- deployed_at %> + </span> + <a class="js-environment-link" href="<%- external_url %>" target="_blank"> + <i class="fa fa-external-link"></i> + View on <%- external_url_formatted %> + </a> + </span> + </div> + </div>`; + + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -10,17 +29,23 @@ // ci_status_url - String, URL to use to check CI status // this.opts = opts; + this.$widgetBody = $('.mr-widget-body'); $('#modal_merge_info').modal({ show: false }); this.firstCICheck = true; this.readyForCICheck = false; + this.readyForCIEnvironmentCheck = false; this.cancel = false; clearInterval(this.fetchBuildStatusInterval); + clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); + this.getCIEnvironmentsStatus(); + this.retrieveSuccessIcon(); this.pollCIStatus(); + this.pollCIEnvironmentsStatus(); notifyPermissions(); } @@ -41,6 +66,7 @@ page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { clearInterval(_this.fetchBuildStatusInterval); + clearInterval(_this.fetchBuildEnvironmentStatusInterval); _this.cancelPolling(); return _this.clearEventListeners(); } @@ -48,6 +74,12 @@ })(this)); }; + MergeRequestWidget.prototype.retrieveSuccessIcon = function() { + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + } + MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { deleteSourceBranch = false; @@ -62,7 +94,7 @@ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); + return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); @@ -118,6 +150,7 @@ if (data.status === '') { return; } + if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); @@ -150,6 +183,41 @@ })(this)); }; + MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { + this.fetchBuildEnvironmentStatusInterval = setInterval(() => { + if (!this.readyForCIEnvironmentCheck) return; + this.getCIEnvironmentsStatus(); + this.readyForCIEnvironmentCheck = false; + }, 300000); + }; + + MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { + $.getJSON(this.opts.ci_environments_status_url, (environments) => { + if (this.cancel) return; + this.readyForCIEnvironmentCheck = true; + if (environments && environments.length) this.renderEnvironments(environments); + }); + }; + + MergeRequestWidget.prototype.renderEnvironments = function(environments) { + for (let i = 0; i < environments.length; i++) { + const environment = environments[i]; + if ($(`.mr-state-widget #${ environment.id }`).length) return; + const $template = $(DEPLOYMENT_TEMPLATE); + if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); + if (environment.deployed_at && environment.deployed_at_formatted) { + environment.deployed_at = $.timeago(environment.deployed_at) + '.'; + } else { + $('.js-environment-timeago', $template).remove(); + environment.name += '.'; + } + environment.ci_success_icon = this.$ciSuccessIcon; + const templateString = _.unescape($template[0].outerHTML); + const template = _.template(templateString)(environment) + this.$widgetBody.before(template); + } + }; + MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; if (state == null) { @@ -190,4 +258,4 @@ })(); -}).call(this); + })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 26cc6eb0e96..cee42633c79 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -110,7 +110,7 @@ e.preventDefault(); return; } - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; gl.issueBoards.BoardsStore.updateFiltersUrl(); e.preventDefault(); diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch_graph.js index 91132af273a..91132af273a 100644 --- a/app/assets/javascripts/network/branch-graph.js +++ b/app/assets/javascripts/network/branch_graph.js diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6 deleted file mode 100644 index bf33eb10100..00000000000 --- a/app/assets/javascripts/pipeline.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -(function() { - function toggleGraph() { - const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); - const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); - const $btnText = $(this).find('.toggle-btn-text'); - - $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); - - const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); - - graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') - } - - $(document).on('click', '.toggle-pipeline-btn', toggleGraph); -})(); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 new file mode 100644 index 00000000000..6bf63ee6979 --- /dev/null +++ b/app/assets/javascripts/pipelines.js.es6 @@ -0,0 +1,40 @@ +((global) => { + + class Pipelines { + constructor() { + $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); + this.addMarginToBuildColumns(); + } + + toggleGraph() { + const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); + const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); + const $btnText = $(this).find('.toggle-btn-text'); + const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); + + $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); + + + graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') + } + + addMarginToBuildColumns() { + const $secondChildBuildNode = $('.build:nth-child(2)'); + if ($secondChildBuildNode.length) { + const $firstChildBuildNode = $secondChildBuildNode.prev('.build'); + const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column'); + const $previousColumn = $multiBuildColumn.prev('.stage-column'); + $multiBuildColumn.addClass('left-margin'); + $firstChildBuildNode.addClass('left-connector'); + $previousColumn.each(function() { + $this = $(this); + if ($('.build', $this).length === 1) $this.addClass('no-margin'); + }); + } + $('.pipeline-graph').removeClass('hidden'); + } + } + + global.Pipelines = Pipelines; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js deleted file mode 100644 index 78f7b48bc7d..00000000000 --- a/app/assets/javascripts/project_members.js +++ /dev/null @@ -1,10 +0,0 @@ -(function() { - this.ProjectMembers = (function() { - function ProjectMembers() { - $('li.project_member').bind('ajax:success', function() { - return $(this).fadeOut(); - }); - } - return ProjectMembers; - })(); -}).call(this); diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index a787b11f2a9..3cf41505814 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -4,7 +4,9 @@ this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = bind(this.toggleSettings, this); - this.$selects = $('.features select'); + this.$selects = $('.features select').filter(function () { + return $(this).data('field'); + }); $('.project-edit-container').on('ajax:before', (function(_this) { return function() { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index 2ecf3b18975..bd4e3c3d00d 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -16,7 +16,13 @@ if (initialQuery.name) this.requestFile(initialQuery); $('.reset-template', this.dropdown.parent()).on('click', () => { - if (this.currentTemplate) this.setInputValueToTemplateContent(false); + this.setInputValueToTemplateContent(); + }); + + $('.no-template', this.dropdown.parent()).on('click', () => { + this.currentTemplate = ''; + this.setInputValueToTemplateContent(); + $('.dropdown-toggle-text', this.dropdown).text('Choose a template'); }); } diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 63bce0a6f6f..dfdfa1e7f75 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -89,7 +89,7 @@ content on the Users#show page. const action = $target.data('action'); const source = $target.attr('href'); this.setTab(source, action); - return this.setCurrentAction(action); + return this.setCurrentAction(source, action); } activateTab(action) { @@ -142,14 +142,9 @@ content on the Users#show page. .toggle(status); } - setCurrentAction(action) { - const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`); - let new_state = this._location.pathname; + setCurrentAction(source, action) { + let new_state = source new_state = new_state.replace(/\/+$/, ''); - new_state = new_state.replace(regExp, ''); - if (action !== this.defaultAction) { - new_state += `/${action}`; - } new_state += this._location.search + this._location.hash; history.replaceState({ turbolinks: true, diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 new file mode 100644 index 00000000000..2517f778365 --- /dev/null +++ b/app/assets/javascripts/username_validator.js.es6 @@ -0,0 +1,133 @@ +((global) => { + const debounceTimeoutDuration = 1000; + const invalidInputClass = 'gl-field-error-outline'; + const successInputClass = 'gl-field-success-outline'; + const unavailableMessageSelector = '.username .validation-error'; + const successMessageSelector = '.username .validation-success'; + const pendingMessageSelector = '.username .validation-pending'; + const invalidMessageSelector = '.username .gl-field-error'; + + class UsernameValidator { + constructor() { + this.inputElement = $('#new_user_username'); + this.inputDomElement = this.inputElement.get(0); + this.state = { + available: false, + valid: false, + pending: false, + empty: true + }; + + const debounceTimeout = _.debounce((username) => { + this.validateUsername(username); + }, debounceTimeoutDuration); + + this.inputElement.on('keyup.username_check', () => { + const username = this.inputElement.val(); + + this.state.valid = this.inputDomElement.validity.valid; + this.state.empty = !username.length; + + if (this.state.valid) { + return debounceTimeout(username); + } + + this.renderState(); + }); + + // Override generic field validation + this.inputElement.on('invalid', this.interceptInvalid.bind(this)); + } + + renderState() { + // Clear all state + this.clearFieldValidationState(); + + if (this.state.valid && this.state.available) { + return this.setSuccessState(); + } + + if (this.state.empty) { + return this.clearFieldValidationState(); + } + + if (this.state.pending) { + return this.setPendingState(); + } + + if (!this.state.available) { + return this.setUnavailableState(); + } + + if (!this.state.valid) { + return this.setInvalidState(); + } + } + + interceptInvalid(event) { + event.preventDefault(); + event.stopPropagation(); + } + + validateUsername(username) { + if (this.state.valid) { + this.state.pending = true; + this.state.available = false; + this.renderState(); + return $.ajax({ + type: 'GET', + url: `/u/${username}/exists`, + dataType: 'json', + success: (res) => this.setAvailabilityState(res.exists) + }); + } + } + + setAvailabilityState(usernameTaken) { + if (usernameTaken) { + this.state.valid = false; + this.state.available = false; + } else { + this.state.available = true; + } + this.state.pending = false; + this.renderState(); + } + + clearFieldValidationState() { + this.inputElement.siblings('p').hide(); + + this.inputElement.removeClass(invalidInputClass) + .removeClass(successInputClass); + } + + setUnavailableState() { + const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); + this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); + $usernameUnavailableMessage.show(); + } + + setSuccessState() { + const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); + this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); + $usernameSuccessMessage.show(); + } + + setPendingState() { + const $usernamePendingMessage = $(pendingMessageSelector); + if (this.state.pending) { + $usernamePendingMessage.show(); + } else { + $usernamePendingMessage.hide(); + } + } + + setInvalidState() { + const $inputErrorMessage = $(invalidMessageSelector); + this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); + $inputErrorMessage.show(); + } + } + + global.UsernameValidator = UsernameValidator; +})(window); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 05056a73aaf..3020b7cc239 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -71,8 +71,8 @@ return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); }; - collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); - assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); + collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); + assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { @@ -160,7 +160,7 @@ selectedId = user.id; return; } - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { selectedId = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.updateFiltersUrl(); @@ -261,10 +261,11 @@ } } if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { + var trimmed = query.term.trim(); emailUser = { name: "Invite \"" + query.term + "\"", - username: query.term, - id: query.term + username: trimmed, + id: trimmed }; data.results.unshift(emailUser); } @@ -324,6 +325,10 @@ }; UsersSelect.prototype.user = function(user_id, callback) { + if(!/^\d+$/.test(user_id)) { + return false; + } + var url; url = this.buildUrl(this.userPath); url = url.replace(':id', user_id); diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 897bc49e7df..e3ca7f6373a 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -5,6 +5,7 @@ display: none; &.hide { display: block; } } + &.open .content { display: block; &.hide { display: none; } diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index c79b22d4d21..98e301d3799 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -4,7 +4,7 @@ width: 40px; height: 40px; padding: 0; - @include border-radius($avatar_radius); + border-radius: $avatar_radius; border: 1px solid rgba(0, 0, 0, .1); &.avatar-inline { @@ -17,7 +17,7 @@ } &.avatar-tile { - @include border-radius(0); + border-radius: 0; border: none; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d315db4cb32..df2e2ea8d2c 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -19,6 +19,7 @@ &.diff-collapsed { padding: 5px; + .click-to-expand { cursor: pointer; } @@ -133,7 +134,7 @@ } .identicon { - @include border-radius(50%); + border-radius: 50%; } } @@ -203,6 +204,7 @@ } } } + &.user-cover-block { padding: 24px 0 0; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ce489f7c3de..e6656c2d69a 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,5 +1,5 @@ @mixin btn-default { - @include border-radius(3px); + border-radius: 3px; font-size: $gl-font-size; font-weight: 500; padding: $gl-vert-padding $gl-btn-padding; @@ -8,7 +8,7 @@ &:active { outline: none; background-color: $btn-active-gray; - @include box-shadow($gl-btn-active-background); + box-shadow: $gl-btn-active-background; } } @@ -25,7 +25,7 @@ &:focus { background-color: $hover-background; color: $hover-text; - border-color: $hover-border;; + border-color: $hover-border; } } @@ -43,7 +43,7 @@ &:active, &.active { - @include box-shadow ($gl-btn-active-background); + box-shadow: $gl-btn-active-background; background-color: $dark; border-color: $border-dark; @@ -152,7 +152,8 @@ @include btn-blue-medium; } - &.btn-info { + &.btn-info, + &.btn-register { @include btn-blue; } @@ -194,10 +195,17 @@ pointer-events: none !important; } - .caret { + .fa-caret-down, + .fa-caret-up { margin-left: 5px; } + &.dropdown-toggle { + .fa-caret-down { + margin-left: 3px; + } + } + svg { height: 15px; width: 15px; @@ -233,6 +241,7 @@ width: 100%; margin: 0; margin-bottom: 15px; + &.btn { padding: 6px 0; } @@ -272,7 +281,7 @@ } .active { - @include box-shadow($gl-btn-active-background); + box-shadow: $gl-btn-active-background; border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; @@ -314,6 +323,7 @@ .btn-build { margin-left: 10px; + i { color: $gl-icon-color; } @@ -321,6 +331,7 @@ .clone-dropdown-btn a { color: $dropdown-link-color; + &:hover { text-decoration: none; } @@ -330,6 +341,7 @@ background-color: $background-color !important; border: 1px solid lightgrey; cursor: default; + &:active { -moz-box-shadow: inset 0 0 0 white; -webkit-box-shadow: inset 0 0 0 white; diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index da7bab74a32..f3b6ad88ad6 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -13,10 +13,12 @@ color: $text-color; background: $background-color; } + .bs-callout h4 { margin-top: 0; margin-bottom: 5px; } + .bs-callout p:last-child { margin-bottom: 0; } @@ -27,16 +29,19 @@ border-color: #eed3d7; color: #b94a48; } + .bs-callout-warning { background-color: #faf8f0; border-color: #faebcc; color: #8a6d3b; } + .bs-callout-info { background-color: #f4f8fa; border-color: #bce8f1; color: #34789a; } + .bs-callout-success { background-color: #dff0d8; border-color: #5ca64d; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5957dce89bc..81e4e264560 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -1,31 +1,31 @@ /** COLORS **/ .cgray { color: $gl-gray; } -.clgray { color: #bbb } +.clgray { color: #bbb; } .cred { color: $gl-text-red; } .cgreen { color: $gl-text-green; } -.cdark { color: #444 } +.cdark { color: #444; } /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-5 { margin-top: 5px; } -.prepend-top-10 { margin-top: 10px } +.prepend-top-10 { margin-top: 10px; } .prepend-top-default { margin-top: $gl-padding !important; } -.prepend-top-20 { margin-top: 20px } -.prepend-left-5 { margin-left: 5px } -.prepend-left-10 { margin-left: 10px } +.prepend-top-20 { margin-top: 20px; } +.prepend-left-5 { margin-left: 5px; } +.prepend-left-10 { margin-left: 10px; } .prepend-left-default { margin-left: $gl-padding; } -.prepend-left-20 { margin-left: 20px } -.append-right-5 { margin-right: 5px } -.append-right-10 { margin-right: 10px } +.prepend-left-20 { margin-left: 20px; } +.append-right-5 { margin-right: 5px; } +.append-right-10 { margin-right: 10px; } .append-right-default { margin-right: $gl-padding; } -.append-right-20 { margin-right: 20px } -.append-bottom-0 { margin-bottom: 0 } -.append-bottom-10 { margin-bottom: 10px } -.append-bottom-15 { margin-bottom: 15px } -.append-bottom-20 { margin-bottom: 20px } +.append-right-20 { margin-right: 20px; } +.append-bottom-0 { margin-bottom: 0; } +.append-bottom-10 { margin-bottom: 10px; } +.append-bottom-15 { margin-bottom: 15px; } +.append-bottom-20 { margin-bottom: 20px; } .append-bottom-default { margin-bottom: $gl-padding; } -.inline { display: inline-block } -.center { text-align: center } +.inline { display: inline-block; } +.center { text-align: center; } .underlined-link { text-decoration: underline; } .hint { font-style: italic; color: #999; } @@ -97,6 +97,7 @@ span.update-author { color: #999; font-weight: normal; font-style: italic; + strong { font-weight: bold; font-style: normal; @@ -128,7 +129,7 @@ p.time { // Fix issue with notes & lists creating a bunch of bottom borders. li.note { - img { max-width: 100% } + img { max-width: 100%; } .note-title { li { border-bottom: none !important; @@ -172,6 +173,7 @@ li.note { @extend .col-md-6; text-align: left; margin-top: 40px; + pre { background: white; border: none; @@ -197,6 +199,7 @@ li.note { background: #c67; color: #fff; font-weight: bold; + a { color: #fff; text-decoration: underline; @@ -227,6 +230,7 @@ li.note { &.milestone-closed { background: $gray-light; } + .progress { margin-bottom: 0; margin-top: 4px; @@ -286,6 +290,7 @@ table { .footer-links { margin-bottom: 20px; + a { margin-right: 15px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 4a87a73a68a..a839371a6f2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -1,20 +1,3 @@ -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: $caret-width-base dashed; - border-right: $caret-width-base solid transparent; - border-left: $caret-width-base solid transparent; -} - -.btn-group { - .caret { - margin-left: 0; - } -} - .dropdown { position: relative; @@ -29,6 +12,7 @@ .dropdown-menu, .dropdown-menu-nav { display: block; + @media (max-width: $screen-xs-max) { width: 100%; } @@ -65,6 +49,7 @@ margin-top: -6px; color: $dropdown-toggle-icon-color; font-size: 10px; + &.fa-spinner { font-size: 16px; margin-top: -8px; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 81520500594..13c1bbf0359 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -26,15 +26,6 @@ padding: 10px $gl-padding; word-wrap: break-word; border-radius: 3px 3px 0 0; - cursor: pointer; - - &:hover { - background-color: $dark-background-color; - } - - .diff-toggle-caret { - padding-right: 6px; - } &.file-title-clear { padding-left: 0; @@ -66,6 +57,7 @@ margin-top: -3px; } } + .file-content { background: #fff; @@ -105,22 +97,27 @@ border: none; margin: 0; } + tr { border-bottom: 1px solid #eee; } + td { &:first-child { border-left: none; } + &:last-child { border-right: none; } } + td.blame-commit { padding: 0 10px; min-width: 400px; background: $gray-light; } + td.line-numbers { float: none; border-left: 1px solid #ddd; @@ -130,6 +127,7 @@ margin-right: 0; } } + td.lines { padding: 0; } @@ -146,8 +144,10 @@ border-left: 1px solid $border-color; margin-bottom: 0; background: white; + li { color: #888; + p { margin: 0; color: #333; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 37ff7e22ed1..761c07384f4 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -9,7 +9,7 @@ input { input[type='text'].danger { background: #f2dede!important; border-color: #d66; - text-shadow: 0 1px 1px #fff + text-shadow: 0 1px 1px #fff; } .datetime-controls { @@ -74,17 +74,17 @@ label { .form-control { @include box-shadow(none); - border-radius: 3px; + border-radius: 2px; padding: $gl-vert-padding $gl-input-padding; } .select-wrapper { position: relative; - .caret { + .fa-caret-down { position: absolute; right: 10px; - top: $gl-padding; + top: 10px; color: $gray-darkest; pointer-events: none; } @@ -117,9 +117,11 @@ label { display: table-cell; width: 200px !important; } + .input-group-addon { background-color: #f7f8fa; } + .input-group-addon:not(:first-child):not(:last-child) { border-left: 0; border-right: 0; @@ -129,3 +131,8 @@ label { .help-block { margin-bottom: 0; } + +.gl-field-error { + color: $red-normal; +} + diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 3673b81f183..fe834f4e2f6 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -62,7 +62,7 @@ } i { - color: $white-light + color: $white-light; } path, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index c748f856501..3a4fdd0da22 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -57,6 +57,10 @@ header { &:hover, &:focus, &:active { background-color: $background-color; } + + .fa-caret-down { + font-size: 15px; + } } .navbar-toggle { @@ -164,6 +168,7 @@ header { a { color: $gl-text-color; + &:hover { text-decoration: underline; } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 8bfc0d583c5..ba3930e03bd 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -16,7 +16,7 @@ margin-top: 5px; } - @include border-radius(3px); + border-radius: 3px; display: block; float: left; margin-right: 10px; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index efc348214c2..4b2627c1b87 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -60,6 +60,7 @@ padding-top: 1px; margin: 0; color: $gray-dark; + img { position: relative; top: 3px; @@ -128,6 +129,10 @@ ul.content-list { color: $gl-dark-link-color; } + .member-group-link { + color: $blue-normal; + } + .description { p { @include str-truncated; @@ -168,6 +173,14 @@ ul.content-list { } } + .member-controls { + float: none; + + @media (min-width: $screen-sm-min) { + float: right; + } + } + // When dragging a list item &.ui-sortable-helper { border-bottom: none; diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss index 3ee3fb4cee5..a90e45bb5f4 100644 --- a/app/assets/stylesheets/framework/logo.scss +++ b/app/assets/stylesheets/framework/logo.scss @@ -1,15 +1,3 @@ -@mixin unique-keyframes { - $animation-name: unique-id(); - @include webkit-prefix(animation-name, $animation-name); - - @-webkit-keyframes #{$animation-name} { - @content; - } - @keyframes #{$animation-name} { - @content; - } -} - @mixin tanuki-logo-colors($path-color) { fill: $path-color; transition: all 0.8s; @@ -20,28 +8,6 @@ } } -@mixin tanuki-second-highlight-animations($tanuki-color) { - @include unique-keyframes { - 10%, 80% { - fill: #{$tanuki-color} - } - 20%, 90% { - fill: lighten($tanuki-color, 25%); - } - } -} - -@mixin tanuki-forth-highlight-animations($tanuki-color) { - @include unique-keyframes { - 30%, 60% { - fill: #{$tanuki-color}; - } - 40%, 70% { - fill: lighten($tanuki-color, 25%); - } - } -} - .tanuki-logo { .tanuki-left-ear, @@ -67,10 +33,11 @@ } .tanuki-left-cheek { - @include unique-keyframes { + @include include-keyframes(animate-tanuki-left-cheek) { 0%, 10%, 100% { fill: lighten($tanuki-yellow, 25%); } + 90% { fill: $tanuki-yellow; } @@ -78,18 +45,35 @@ } .tanuki-left-eye { - @include tanuki-second-highlight-animations($tanuki-orange); + @include include-keyframes(animate-tanuki-left-eye) { + 10%, 80% { + fill: $tanuki-orange; + } + + 20%, 90% { + fill: lighten($tanuki-orange, 25%); + } + } } .tanuki-left-ear { - @include tanuki-second-highlight-animations($tanuki-red); + @include include-keyframes(animate-tanuki-left-ear) { + 10%, 80% { + fill: $tanuki-red; + } + + 20%, 90% { + fill: lighten($tanuki-red, 25%); + } + } } .tanuki-nose { - @include unique-keyframes { + @include include-keyframes(animate-tanuki-nose) { 20%, 70% { fill: $tanuki-red; } + 30%, 80% { fill: lighten($tanuki-red, 25%); } @@ -97,22 +81,39 @@ } .tanuki-right-eye { - @include tanuki-forth-highlight-animations($tanuki-orange); + @include include-keyframes(animate-tanuki-right-eye) { + 30%, 60% { + fill: $tanuki-orange; + } + + 40%, 70% { + fill: lighten($tanuki-orange, 25%); + } + } } .tanuki-right-ear { - @include tanuki-forth-highlight-animations($tanuki-red); + @include include-keyframes(animate-tanuki-right-ear) { + 30%, 60% { + fill: $tanuki-red; + } + + 40%, 70% { + fill: lighten($tanuki-red, 25%); + } + } } .tanuki-right-cheek { - @include unique-keyframes { + @include include-keyframes(animate-tanuki-right-cheek) { 40% { fill: $tanuki-yellow; } + 60% { fill: lighten($tanuki-yellow, 25%); } } } } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index edea4ad00eb..6d28d98b283 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -86,7 +86,7 @@ } .markdown-area { - @include border-radius(0); + border-radius: 0; background: #fff; border: 1px solid #ddd; min-height: 140px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1ec08cdef23..f84ca36d10f 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -1,15 +1,4 @@ /** - * Generic mixins - */ -@mixin box-shadow($shadow) { - box-shadow: $shadow; -} - -@mixin border-radius($radius) { - border-radius: $radius; -} - -/** * Prefilled mixins * Mixins with fixed values */ @@ -45,6 +34,7 @@ &.active { background: $gray-light; + a { font-weight: 600; } @@ -95,3 +85,10 @@ @content; } } + +@mixin include-keyframes($animation-name) { + @include webkit-prefix(animation-name, $animation-name); + @include keyframes($animation-name) { + @content; + } +} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 76b93b23b95..9fe390eb09d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -133,5 +133,5 @@ font-size: 20px; color: #777; z-index: 100; - @include box-shadow(0 1px 2px #ddd); + box-shadow: 0 1px 2px #ddd; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ea43f4afc37..899db045b74 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -210,6 +210,7 @@ @media (max-width: $screen-xs-max) { padding-bottom: 0; width: 100%; + .btn, form, .dropdown, .dropdown-menu-toggle, .form-control { margin: 0 0 10px; display: block; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index c6f30e144fd..5ba0486177f 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -13,6 +13,11 @@ .dropdown-menu-toggle { line-height: 20px; } + + .badge { + margin-top: -2px; + margin-left: 5px; + } } .panel-body { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index c75dacf95d9..e0708c65695 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -21,7 +21,14 @@ padding-right: 10px; b { - @extend .caret; + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: $caret-width-base dashed; + border-right: $caret-width-base solid transparent; + border-left: $caret-width-base solid transparent; color: $gray-darkest; } } @@ -39,8 +46,8 @@ } .select2-drop { - @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); - @include border-radius ($border-radius-default); + box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0; + border-radius: $border-radius-default; border: none; min-width: 175px; } @@ -65,7 +72,7 @@ .select2-container-active { .select2-choice, .select2-choices { - @include box-shadow(none); + box-shadow: none; } } @@ -75,18 +82,18 @@ outline: 0; background-image: none; background-color: $white-dark; - @include box-shadow($gl-btn-active-gradient); + box-shadow: $gl-btn-active-gradient; } } .select2-container-multi { .select2-choices { - @include border-radius($border-radius-default); + border-radius: $border-radius-default; border-color: $input-border; background: none; .select2-search-field input { - padding: $gl-padding / 2; + padding: 5px $gl-padding / 2; font-size: 13px; height: auto; font-family: inherit; @@ -94,7 +101,7 @@ } .select2-search-choice { - margin: 8px 0 0 8px; + margin: 5px 0 0 8px; box-shadow: none; border-color: $input-border; color: $gl-text-color; @@ -116,7 +123,7 @@ &.select2-container-active .select2-choices, &.select2-dropdown-open .select2-choices { border-color: $border-white-normal; - @include box-shadow($gl-btn-active-gradient); + box-shadow: $gl-btn-active-gradient; } } @@ -130,6 +137,7 @@ .select2-results { max-height: 350px; + .select2-highlighted { background: $gl-primary; } @@ -150,7 +158,7 @@ background-repeat: no-repeat; background-position: right 0 bottom 6px; border: 1px solid $input-border; - @include border-radius($border-radius-default); + border-radius: $border-radius-default; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { @@ -205,9 +213,11 @@ .group-image { float: left; } + .group-name { font-weight: bold; } + .group-path { color: #999; } @@ -232,6 +242,7 @@ color: #aaa; font-weight: normal; } + .namespace-path { margin-left: 10px; font-weight: bolder; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 557ef7291cf..ec52f326eb9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -4,7 +4,7 @@ &.page-sidebar-pinned { .sidebar-wrapper { - @include box-shadow(none); + box-shadow: none; } } @@ -17,7 +17,7 @@ width: 0; overflow: hidden; transition: width $sidebar-transition-duration; - @include box-shadow(2px 0 16px 0 $black-transparent); + box-shadow: 2px 0 16px 0 $black-transparent; } } @@ -100,7 +100,7 @@ .count { float: right; padding: 0 8px; - @include border-radius(6px); + border-radius: 6px; } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 0b0bd80c326..eb63a9f214b 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -48,6 +48,7 @@ &:before { background: none; } + .timeline-entry .timeline-entry-inner { .timeline-icon { display: none; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index e3154657c54..f4106641269 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -48,31 +48,40 @@ .clearfix { @include clearfix(); } + .center-block { @include center-block(); } + .pull-right { float: right !important; } + .pull-left { float: left !important; } + .hide { display: none; } + .show { display: block !important; } + .invisible { visibility: hidden; } + .text-hide { @include text-hide(); } + .hidden { display: none !important; visibility: hidden !important; } + .affix { position: fixed; } @@ -146,6 +155,7 @@ padding: 6px 15px; font-size: 13px; font-weight: normal; + a { color: #777; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9f2d53d5206..8df0067fac1 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -106,6 +106,7 @@ @extend .table-bordered; margin: 12px 0; color: #5c5d5e; + th { background: #f8fafc; } @@ -116,7 +117,7 @@ font-size: 13px; line-height: 1.6em; overflow-x: auto; - @include border-radius(2px); + border-radius: 2px; } p > code { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 14ec310de2d..4c34ed3ebf7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -17,8 +17,10 @@ $white-normal: #ededed; $white-dark: #ececec; $gray-light: #fafafa; +$gray-lighter: #f9f9f9; $gray-normal: #f5f5f5; $gray-dark: #ededed; +$gray-darker: #eee; $gray-darkest: #c9c9c9; $green-light: #38ae67; @@ -33,6 +35,8 @@ $blue-medium-light: #3498cb; $blue-medium: #2f8ebf; $blue-medium-dark: #2d86b4; +$blue-light-transparent: rgba(44, 159, 216, 0.05); + $orange-light: #fc8a51; $orange-normal: #e75e40; $orange-dark: #ce5237; @@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f; $gl-font-size: 15px; $gl-title-color: #333; $gl-text-color: #5c5c5c; +$gl-text-color-light: #8c8c8c; $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 16ffbe57a99..a3acee299e3 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -55,68 +55,68 @@ color: #000 !important; } - .hll { background-color: #373b41 } - .c { color: #969896 } /* Comment */ - .err { color: #c66 } /* Error */ - .k { color: #b294bb } /* Keyword */ - .l { color: #de935f } /* Literal */ - .n { color: #c5c8c6 } /* Name */ - .o { color: #8abeb7 } /* Operator */ - .p { color: #c5c8c6 } /* Punctuation */ - .cm { color: #969896 } /* Comment.Multiline */ - .cp { color: #969896 } /* Comment.Preproc */ - .c1 { color: #969896 } /* Comment.Single */ - .cs { color: #969896 } /* Comment.Special */ - .gd { color: #c66 } /* Generic.Deleted */ - .ge { font-style: italic } /* Generic.Emph */ - .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */ - .gi { color: #b5bd68 } /* Generic.Inserted */ - .gp { color: #969896; font-weight: bold } /* Generic.Prompt */ - .gs { font-weight: bold } /* Generic.Strong */ - .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */ - .kc { color: #b294bb } /* Keyword.Constant */ - .kd { color: #b294bb } /* Keyword.Declaration */ - .kn { color: #8abeb7 } /* Keyword.Namespace */ - .kp { color: #b294bb } /* Keyword.Pseudo */ - .kr { color: #b294bb } /* Keyword.Reserved */ - .kt { color: #f0c674 } /* Keyword.Type */ - .ld { color: #b5bd68 } /* Literal.Date */ - .m { color: #de935f } /* Literal.Number */ - .s { color: #b5bd68 } /* Literal.String */ - .na { color: #81a2be } /* Name.Attribute */ - .nb { color: #c5c8c6 } /* Name.Builtin */ - .nc { color: #f0c674 } /* Name.Class */ - .no { color: #c66 } /* Name.Constant */ - .nd { color: #8abeb7 } /* Name.Decorator */ - .ni { color: #c5c8c6 } /* Name.Entity */ - .ne { color: #c66 } /* Name.Exception */ - .nf { color: #81a2be } /* Name.Function */ - .nl { color: #c5c8c6 } /* Name.Label */ - .nn { color: #f0c674 } /* Name.Namespace */ - .nx { color: #81a2be } /* Name.Other */ - .py { color: #c5c8c6 } /* Name.Property */ - .nt { color: #8abeb7 } /* Name.Tag */ - .nv { color: #c66 } /* Name.Variable */ - .ow { color: #8abeb7 } /* Operator.Word */ - .w { color: #c5c8c6 } /* Text.Whitespace */ - .mf { color: #de935f } /* Literal.Number.Float */ - .mh { color: #de935f } /* Literal.Number.Hex */ - .mi { color: #de935f } /* Literal.Number.Integer */ - .mo { color: #de935f } /* Literal.Number.Oct */ - .sb { color: #b5bd68 } /* Literal.String.Backtick */ - .sc { color: #c5c8c6 } /* Literal.String.Char */ - .sd { color: #969896 } /* Literal.String.Doc */ - .s2 { color: #b5bd68 } /* Literal.String.Double */ - .se { color: #de935f } /* Literal.String.Escape */ - .sh { color: #b5bd68 } /* Literal.String.Heredoc */ - .si { color: #de935f } /* Literal.String.Interpol */ - .sx { color: #b5bd68 } /* Literal.String.Other */ - .sr { color: #b5bd68 } /* Literal.String.Regex */ - .s1 { color: #b5bd68 } /* Literal.String.Single */ - .ss { color: #b5bd68 } /* Literal.String.Symbol */ - .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */ - .vc { color: #c66 } /* Name.Variable.Class */ - .vg { color: #c66 } /* Name.Variable.Global */ - .vi { color: #c66 } /* Name.Variable.Instance */ - .il { color: #de935f } /* Literal.Number.Integer.Long */ + .hll { background-color: #373b41; } + .c { color: #969896; } /* Comment */ + .err { color: #c66; } /* Error */ + .k { color: #b294bb; } /* Keyword */ + .l { color: #de935f; } /* Literal */ + .n { color: #c5c8c6; } /* Name */ + .o { color: #8abeb7; } /* Operator */ + .p { color: #c5c8c6; } /* Punctuation */ + .cm { color: #969896; } /* Comment.Multiline */ + .cp { color: #969896; } /* Comment.Preproc */ + .c1 { color: #969896; } /* Comment.Single */ + .cs { color: #969896; } /* Comment.Special */ + .gd { color: #c66; } /* Generic.Deleted */ + .ge { font-style: italic; } /* Generic.Emph */ + .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */ + .gi { color: #b5bd68; } /* Generic.Inserted */ + .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */ + .gs { font-weight: bold; } /* Generic.Strong */ + .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */ + .kc { color: #b294bb; } /* Keyword.Constant */ + .kd { color: #b294bb; } /* Keyword.Declaration */ + .kn { color: #8abeb7; } /* Keyword.Namespace */ + .kp { color: #b294bb; } /* Keyword.Pseudo */ + .kr { color: #b294bb; } /* Keyword.Reserved */ + .kt { color: #f0c674; } /* Keyword.Type */ + .ld { color: #b5bd68; } /* Literal.Date */ + .m { color: #de935f; } /* Literal.Number */ + .s { color: #b5bd68; } /* Literal.String */ + .na { color: #81a2be; } /* Name.Attribute */ + .nb { color: #c5c8c6; } /* Name.Builtin */ + .nc { color: #f0c674; } /* Name.Class */ + .no { color: #c66; } /* Name.Constant */ + .nd { color: #8abeb7; } /* Name.Decorator */ + .ni { color: #c5c8c6; } /* Name.Entity */ + .ne { color: #c66; } /* Name.Exception */ + .nf { color: #81a2be; } /* Name.Function */ + .nl { color: #c5c8c6; } /* Name.Label */ + .nn { color: #f0c674; } /* Name.Namespace */ + .nx { color: #81a2be; } /* Name.Other */ + .py { color: #c5c8c6; } /* Name.Property */ + .nt { color: #8abeb7; } /* Name.Tag */ + .nv { color: #c66; } /* Name.Variable */ + .ow { color: #8abeb7; } /* Operator.Word */ + .w { color: #c5c8c6; } /* Text.Whitespace */ + .mf { color: #de935f; } /* Literal.Number.Float */ + .mh { color: #de935f; } /* Literal.Number.Hex */ + .mi { color: #de935f; } /* Literal.Number.Integer */ + .mo { color: #de935f; } /* Literal.Number.Oct */ + .sb { color: #b5bd68; } /* Literal.String.Backtick */ + .sc { color: #c5c8c6; } /* Literal.String.Char */ + .sd { color: #969896; } /* Literal.String.Doc */ + .s2 { color: #b5bd68; } /* Literal.String.Double */ + .se { color: #de935f; } /* Literal.String.Escape */ + .sh { color: #b5bd68; } /* Literal.String.Heredoc */ + .si { color: #de935f; } /* Literal.String.Interpol */ + .sx { color: #b5bd68; } /* Literal.String.Other */ + .sr { color: #b5bd68; } /* Literal.String.Regex */ + .s1 { color: #b5bd68; } /* Literal.String.Single */ + .ss { color: #b5bd68; } /* Literal.String.Symbol */ + .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */ + .vc { color: #c66; } /* Name.Variable.Class */ + .vg { color: #c66; } /* Name.Variable.Global */ + .vi { color: #c66; } /* Name.Variable.Instance */ + .il { color: #de935f; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 7de920e074b..e9228c94db9 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -55,65 +55,65 @@ color: #000 !important; } - .hll { background-color: #49483e } - .c { color: #75715e } /* Comment */ - .err { color: #960050; background-color: #1e0010 } /* Error */ - .k { color: #66d9ef } /* Keyword */ - .l { color: #ae81ff } /* Literal */ - .n { color: #f8f8f2 } /* Name */ - .o { color: #f92672 } /* Operator */ - .p { color: #f8f8f2 } /* Punctuation */ - .cm { color: #75715e } /* Comment.Multiline */ - .cp { color: #75715e } /* Comment.Preproc */ - .c1 { color: #75715e } /* Comment.Single */ - .cs { color: #75715e } /* Comment.Special */ - .ge { font-style: italic } /* Generic.Emph */ - .gs { font-weight: bold } /* Generic.Strong */ - .kc { color: #66d9ef } /* Keyword.Constant */ - .kd { color: #66d9ef } /* Keyword.Declaration */ - .kn { color: #f92672 } /* Keyword.Namespace */ - .kp { color: #66d9ef } /* Keyword.Pseudo */ - .kr { color: #66d9ef } /* Keyword.Reserved */ - .kt { color: #66d9ef } /* Keyword.Type */ - .ld { color: #e6db74 } /* Literal.Date */ - .m { color: #ae81ff } /* Literal.Number */ - .s { color: #e6db74 } /* Literal.String */ - .na { color: #a6e22e } /* Name.Attribute */ - .nb { color: #f8f8f2 } /* Name.Builtin */ - .nc { color: #a6e22e } /* Name.Class */ - .no { color: #66d9ef } /* Name.Constant */ - .nd { color: #a6e22e } /* Name.Decorator */ - .ni { color: #f8f8f2 } /* Name.Entity */ - .ne { color: #a6e22e } /* Name.Exception */ - .nf { color: #a6e22e } /* Name.Function */ - .nl { color: #f8f8f2 } /* Name.Label */ - .nn { color: #f8f8f2 } /* Name.Namespace */ - .nx { color: #a6e22e } /* Name.Other */ - .py { color: #f8f8f2 } /* Name.Property */ - .nt { color: #f92672 } /* Name.Tag */ - .nv { color: #f8f8f2 } /* Name.Variable */ - .ow { color: #f92672 } /* Operator.Word */ - .w { color: #f8f8f2 } /* Text.Whitespace */ - .mf { color: #ae81ff } /* Literal.Number.Float */ - .mh { color: #ae81ff } /* Literal.Number.Hex */ - .mi { color: #ae81ff } /* Literal.Number.Integer */ - .mo { color: #ae81ff } /* Literal.Number.Oct */ - .sb { color: #e6db74 } /* Literal.String.Backtick */ - .sc { color: #e6db74 } /* Literal.String.Char */ - .sd { color: #e6db74 } /* Literal.String.Doc */ - .s2 { color: #e6db74 } /* Literal.String.Double */ - .se { color: #ae81ff } /* Literal.String.Escape */ - .sh { color: #e6db74 } /* Literal.String.Heredoc */ - .si { color: #e6db74 } /* Literal.String.Interpol */ - .sx { color: #e6db74 } /* Literal.String.Other */ - .sr { color: #e6db74 } /* Literal.String.Regex */ - .s1 { color: #e6db74 } /* Literal.String.Single */ - .ss { color: #e6db74 } /* Literal.String.Symbol */ - .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ - .vc { color: #f8f8f2 } /* Name.Variable.Class */ - .vg { color: #f8f8f2 } /* Name.Variable.Global */ - .vi { color: #f8f8f2 } /* Name.Variable.Instance */ - .il { color: #ae81ff } /* Literal.Number.Integer.Long */ + .hll { background-color: #49483e; } + .c { color: #75715e; } /* Comment */ + .err { color: #960050; background-color: #1e0010; } /* Error */ + .k { color: #66d9ef; } /* Keyword */ + .l { color: #ae81ff; } /* Literal */ + .n { color: #f8f8f2; } /* Name */ + .o { color: #f92672; } /* Operator */ + .p { color: #f8f8f2; } /* Punctuation */ + .cm { color: #75715e; } /* Comment.Multiline */ + .cp { color: #75715e; } /* Comment.Preproc */ + .c1 { color: #75715e; } /* Comment.Single */ + .cs { color: #75715e; } /* Comment.Special */ + .ge { font-style: italic; } /* Generic.Emph */ + .gs { font-weight: bold; } /* Generic.Strong */ + .kc { color: #66d9ef; } /* Keyword.Constant */ + .kd { color: #66d9ef; } /* Keyword.Declaration */ + .kn { color: #f92672; } /* Keyword.Namespace */ + .kp { color: #66d9ef; } /* Keyword.Pseudo */ + .kr { color: #66d9ef; } /* Keyword.Reserved */ + .kt { color: #66d9ef; } /* Keyword.Type */ + .ld { color: #e6db74; } /* Literal.Date */ + .m { color: #ae81ff; } /* Literal.Number */ + .s { color: #e6db74; } /* Literal.String */ + .na { color: #a6e22e; } /* Name.Attribute */ + .nb { color: #f8f8f2; } /* Name.Builtin */ + .nc { color: #a6e22e; } /* Name.Class */ + .no { color: #66d9ef; } /* Name.Constant */ + .nd { color: #a6e22e; } /* Name.Decorator */ + .ni { color: #f8f8f2; } /* Name.Entity */ + .ne { color: #a6e22e; } /* Name.Exception */ + .nf { color: #a6e22e; } /* Name.Function */ + .nl { color: #f8f8f2; } /* Name.Label */ + .nn { color: #f8f8f2; } /* Name.Namespace */ + .nx { color: #a6e22e; } /* Name.Other */ + .py { color: #f8f8f2; } /* Name.Property */ + .nt { color: #f92672; } /* Name.Tag */ + .nv { color: #f8f8f2; } /* Name.Variable */ + .ow { color: #f92672; } /* Operator.Word */ + .w { color: #f8f8f2; } /* Text.Whitespace */ + .mf { color: #ae81ff; } /* Literal.Number.Float */ + .mh { color: #ae81ff; } /* Literal.Number.Hex */ + .mi { color: #ae81ff; } /* Literal.Number.Integer */ + .mo { color: #ae81ff; } /* Literal.Number.Oct */ + .sb { color: #e6db74; } /* Literal.String.Backtick */ + .sc { color: #e6db74; } /* Literal.String.Char */ + .sd { color: #e6db74; } /* Literal.String.Doc */ + .s2 { color: #e6db74; } /* Literal.String.Double */ + .se { color: #ae81ff; } /* Literal.String.Escape */ + .sh { color: #e6db74; } /* Literal.String.Heredoc */ + .si { color: #e6db74; } /* Literal.String.Interpol */ + .sx { color: #e6db74; } /* Literal.String.Other */ + .sr { color: #e6db74; } /* Literal.String.Regex */ + .s1 { color: #e6db74; } /* Literal.String.Single */ + .ss { color: #e6db74; } /* Literal.String.Symbol */ + .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */ + .vc { color: #f8f8f2; } /* Name.Variable.Class */ + .vg { color: #f8f8f2; } /* Name.Variable.Global */ + .vi { color: #f8f8f2; } /* Name.Variable.Instance */ + .il { color: #ae81ff; } /* Literal.Number.Integer.Long */ .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index b11499c71ee..c3c7773b9e2 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -72,72 +72,72 @@ green #859900 operators, other keywords */ - .c { color: #586e75 } /* Comment */ - .err { color: #93a1a1 } /* Error */ - .g { color: #93a1a1 } /* Generic */ - .k { color: #859900 } /* Keyword */ - .l { color: #93a1a1 } /* Literal */ - .n { color: #93a1a1 } /* Name */ - .o { color: #859900 } /* Operator */ - .x { color: #cb4b16 } /* Other */ - .p { color: #93a1a1 } /* Punctuation */ - .cm { color: #586e75 } /* Comment.Multiline */ - .cp { color: #859900 } /* Comment.Preproc */ - .c1 { color: #586e75 } /* Comment.Single */ - .cs { color: #859900 } /* Comment.Special */ - .gd { color: #2aa198 } /* Generic.Deleted */ - .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ - .gr { color: #dc322f } /* Generic.Error */ - .gh { color: #cb4b16 } /* Generic.Heading */ - .gi { color: #859900 } /* Generic.Inserted */ - .go { color: #93a1a1 } /* Generic.Output */ - .gp { color: #93a1a1 } /* Generic.Prompt */ - .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ - .gu { color: #cb4b16 } /* Generic.Subheading */ - .gt { color: #93a1a1 } /* Generic.Traceback */ - .kc { color: #cb4b16 } /* Keyword.Constant */ - .kd { color: #268bd2 } /* Keyword.Declaration */ - .kn { color: #859900 } /* Keyword.Namespace */ - .kp { color: #859900 } /* Keyword.Pseudo */ - .kr { color: #268bd2 } /* Keyword.Reserved */ - .kt { color: #dc322f } /* Keyword.Type */ - .ld { color: #93a1a1 } /* Literal.Date */ - .m { color: #2aa198 } /* Literal.Number */ - .s { color: #2aa198 } /* Literal.String */ - .na { color: #93a1a1 } /* Name.Attribute */ - .nb { color: #b58900 } /* Name.Builtin */ - .nc { color: #268bd2 } /* Name.Class */ - .no { color: #cb4b16 } /* Name.Constant */ - .nd { color: #268bd2 } /* Name.Decorator */ - .ni { color: #cb4b16 } /* Name.Entity */ - .ne { color: #cb4b16 } /* Name.Exception */ - .nf { color: #268bd2 } /* Name.Function */ - .nl { color: #93a1a1 } /* Name.Label */ - .nn { color: #93a1a1 } /* Name.Namespace */ - .nx { color: #93a1a1 } /* Name.Other */ - .py { color: #93a1a1 } /* Name.Property */ - .nt { color: #268bd2 } /* Name.Tag */ - .nv { color: #268bd2 } /* Name.Variable */ - .ow { color: #859900 } /* Operator.Word */ - .w { color: #93a1a1 } /* Text.Whitespace */ - .mf { color: #2aa198 } /* Literal.Number.Float */ - .mh { color: #2aa198 } /* Literal.Number.Hex */ - .mi { color: #2aa198 } /* Literal.Number.Integer */ - .mo { color: #2aa198 } /* Literal.Number.Oct */ - .sb { color: #586e75 } /* Literal.String.Backtick */ - .sc { color: #2aa198 } /* Literal.String.Char */ - .sd { color: #93a1a1 } /* Literal.String.Doc */ - .s2 { color: #2aa198 } /* Literal.String.Double */ - .se { color: #cb4b16 } /* Literal.String.Escape */ - .sh { color: #93a1a1 } /* Literal.String.Heredoc */ - .si { color: #2aa198 } /* Literal.String.Interpol */ - .sx { color: #2aa198 } /* Literal.String.Other */ - .sr { color: #dc322f } /* Literal.String.Regex */ - .s1 { color: #2aa198 } /* Literal.String.Single */ - .ss { color: #2aa198 } /* Literal.String.Symbol */ - .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2 } /* Name.Variable.Class */ - .vg { color: #268bd2 } /* Name.Variable.Global */ - .vi { color: #268bd2 } /* Name.Variable.Instance */ - .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + .c { color: #586e75; } /* Comment */ + .err { color: #93a1a1; } /* Error */ + .g { color: #93a1a1; } /* Generic */ + .k { color: #859900; } /* Keyword */ + .l { color: #93a1a1; } /* Literal */ + .n { color: #93a1a1; } /* Name */ + .o { color: #859900; } /* Operator */ + .x { color: #cb4b16; } /* Other */ + .p { color: #93a1a1; } /* Punctuation */ + .cm { color: #586e75; } /* Comment.Multiline */ + .cp { color: #859900; } /* Comment.Preproc */ + .c1 { color: #586e75; } /* Comment.Single */ + .cs { color: #859900; } /* Comment.Special */ + .gd { color: #2aa198; } /* Generic.Deleted */ + .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */ + .gr { color: #dc322f; } /* Generic.Error */ + .gh { color: #cb4b16; } /* Generic.Heading */ + .gi { color: #859900; } /* Generic.Inserted */ + .go { color: #93a1a1; } /* Generic.Output */ + .gp { color: #93a1a1; } /* Generic.Prompt */ + .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */ + .gu { color: #cb4b16; } /* Generic.Subheading */ + .gt { color: #93a1a1; } /* Generic.Traceback */ + .kc { color: #cb4b16; } /* Keyword.Constant */ + .kd { color: #268bd2; } /* Keyword.Declaration */ + .kn { color: #859900; } /* Keyword.Namespace */ + .kp { color: #859900; } /* Keyword.Pseudo */ + .kr { color: #268bd2; } /* Keyword.Reserved */ + .kt { color: #dc322f; } /* Keyword.Type */ + .ld { color: #93a1a1; } /* Literal.Date */ + .m { color: #2aa198; } /* Literal.Number */ + .s { color: #2aa198; } /* Literal.String */ + .na { color: #93a1a1; } /* Name.Attribute */ + .nb { color: #b58900; } /* Name.Builtin */ + .nc { color: #268bd2; } /* Name.Class */ + .no { color: #cb4b16; } /* Name.Constant */ + .nd { color: #268bd2; } /* Name.Decorator */ + .ni { color: #cb4b16; } /* Name.Entity */ + .ne { color: #cb4b16; } /* Name.Exception */ + .nf { color: #268bd2; } /* Name.Function */ + .nl { color: #93a1a1; } /* Name.Label */ + .nn { color: #93a1a1; } /* Name.Namespace */ + .nx { color: #93a1a1; } /* Name.Other */ + .py { color: #93a1a1; } /* Name.Property */ + .nt { color: #268bd2; } /* Name.Tag */ + .nv { color: #268bd2; } /* Name.Variable */ + .ow { color: #859900; } /* Operator.Word */ + .w { color: #93a1a1; } /* Text.Whitespace */ + .mf { color: #2aa198; } /* Literal.Number.Float */ + .mh { color: #2aa198; } /* Literal.Number.Hex */ + .mi { color: #2aa198; } /* Literal.Number.Integer */ + .mo { color: #2aa198; } /* Literal.Number.Oct */ + .sb { color: #586e75; } /* Literal.String.Backtick */ + .sc { color: #2aa198; } /* Literal.String.Char */ + .sd { color: #93a1a1; } /* Literal.String.Doc */ + .s2 { color: #2aa198; } /* Literal.String.Double */ + .se { color: #cb4b16; } /* Literal.String.Escape */ + .sh { color: #93a1a1; } /* Literal.String.Heredoc */ + .si { color: #2aa198; } /* Literal.String.Interpol */ + .sx { color: #2aa198; } /* Literal.String.Other */ + .sr { color: #dc322f; } /* Literal.String.Regex */ + .s1 { color: #2aa198; } /* Literal.String.Single */ + .ss { color: #2aa198; } /* Literal.String.Symbol */ + .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ + .vc { color: #268bd2; } /* Name.Variable.Class */ + .vg { color: #268bd2; } /* Name.Variable.Global */ + .vi { color: #268bd2; } /* Name.Variable.Instance */ + .il { color: #2aa198; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 657bb5e3cd9..5956a28cafe 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -78,72 +78,72 @@ green #859900 operators, other keywords */ - .c { color: #93a1a1 } /* Comment */ - .err { color: #586e75 } /* Error */ - .g { color: #586e75 } /* Generic */ - .k { color: #859900 } /* Keyword */ - .l { color: #586e75 } /* Literal */ - .n { color: #586e75 } /* Name */ - .o { color: #859900 } /* Operator */ - .x { color: #cb4b16 } /* Other */ - .p { color: #586e75 } /* Punctuation */ - .cm { color: #93a1a1 } /* Comment.Multiline */ - .cp { color: #859900 } /* Comment.Preproc */ - .c1 { color: #93a1a1 } /* Comment.Single */ - .cs { color: #859900 } /* Comment.Special */ - .gd { color: #2aa198 } /* Generic.Deleted */ - .ge { color: #586e75; font-style: italic } /* Generic.Emph */ - .gr { color: #dc322f } /* Generic.Error */ - .gh { color: #cb4b16 } /* Generic.Heading */ - .gi { color: #859900 } /* Generic.Inserted */ - .go { color: #586e75 } /* Generic.Output */ - .gp { color: #586e75 } /* Generic.Prompt */ - .gs { color: #586e75; font-weight: bold } /* Generic.Strong */ - .gu { color: #cb4b16 } /* Generic.Subheading */ - .gt { color: #586e75 } /* Generic.Traceback */ - .kc { color: #cb4b16 } /* Keyword.Constant */ - .kd { color: #268bd2 } /* Keyword.Declaration */ - .kn { color: #859900 } /* Keyword.Namespace */ - .kp { color: #859900 } /* Keyword.Pseudo */ - .kr { color: #268bd2 } /* Keyword.Reserved */ - .kt { color: #dc322f } /* Keyword.Type */ - .ld { color: #586e75 } /* Literal.Date */ - .m { color: #2aa198 } /* Literal.Number */ - .s { color: #2aa198 } /* Literal.String */ - .na { color: #586e75 } /* Name.Attribute */ - .nb { color: #b58900 } /* Name.Builtin */ - .nc { color: #268bd2 } /* Name.Class */ - .no { color: #cb4b16 } /* Name.Constant */ - .nd { color: #268bd2 } /* Name.Decorator */ - .ni { color: #cb4b16 } /* Name.Entity */ - .ne { color: #cb4b16 } /* Name.Exception */ - .nf { color: #268bd2 } /* Name.Function */ - .nl { color: #586e75 } /* Name.Label */ - .nn { color: #586e75 } /* Name.Namespace */ - .nx { color: #586e75 } /* Name.Other */ - .py { color: #586e75 } /* Name.Property */ - .nt { color: #268bd2 } /* Name.Tag */ - .nv { color: #268bd2 } /* Name.Variable */ - .ow { color: #859900 } /* Operator.Word */ - .w { color: #586e75 } /* Text.Whitespace */ - .mf { color: #2aa198 } /* Literal.Number.Float */ - .mh { color: #2aa198 } /* Literal.Number.Hex */ - .mi { color: #2aa198 } /* Literal.Number.Integer */ - .mo { color: #2aa198 } /* Literal.Number.Oct */ - .sb { color: #93a1a1 } /* Literal.String.Backtick */ - .sc { color: #2aa198 } /* Literal.String.Char */ - .sd { color: #586e75 } /* Literal.String.Doc */ - .s2 { color: #2aa198 } /* Literal.String.Double */ - .se { color: #cb4b16 } /* Literal.String.Escape */ - .sh { color: #586e75 } /* Literal.String.Heredoc */ - .si { color: #2aa198 } /* Literal.String.Interpol */ - .sx { color: #2aa198 } /* Literal.String.Other */ - .sr { color: #dc322f } /* Literal.String.Regex */ - .s1 { color: #2aa198 } /* Literal.String.Single */ - .ss { color: #2aa198 } /* Literal.String.Symbol */ - .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2 } /* Name.Variable.Class */ - .vg { color: #268bd2 } /* Name.Variable.Global */ - .vi { color: #268bd2 } /* Name.Variable.Instance */ - .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + .c { color: #93a1a1; } /* Comment */ + .err { color: #586e75; } /* Error */ + .g { color: #586e75; } /* Generic */ + .k { color: #859900; } /* Keyword */ + .l { color: #586e75; } /* Literal */ + .n { color: #586e75; } /* Name */ + .o { color: #859900; } /* Operator */ + .x { color: #cb4b16; } /* Other */ + .p { color: #586e75; } /* Punctuation */ + .cm { color: #93a1a1; } /* Comment.Multiline */ + .cp { color: #859900; } /* Comment.Preproc */ + .c1 { color: #93a1a1; } /* Comment.Single */ + .cs { color: #859900; } /* Comment.Special */ + .gd { color: #2aa198; } /* Generic.Deleted */ + .ge { color: #586e75; font-style: italic; } /* Generic.Emph */ + .gr { color: #dc322f; } /* Generic.Error */ + .gh { color: #cb4b16; } /* Generic.Heading */ + .gi { color: #859900; } /* Generic.Inserted */ + .go { color: #586e75; } /* Generic.Output */ + .gp { color: #586e75; } /* Generic.Prompt */ + .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */ + .gu { color: #cb4b16; } /* Generic.Subheading */ + .gt { color: #586e75; } /* Generic.Traceback */ + .kc { color: #cb4b16; } /* Keyword.Constant */ + .kd { color: #268bd2; } /* Keyword.Declaration */ + .kn { color: #859900; } /* Keyword.Namespace */ + .kp { color: #859900; } /* Keyword.Pseudo */ + .kr { color: #268bd2; } /* Keyword.Reserved */ + .kt { color: #dc322f; } /* Keyword.Type */ + .ld { color: #586e75; } /* Literal.Date */ + .m { color: #2aa198; } /* Literal.Number */ + .s { color: #2aa198; } /* Literal.String */ + .na { color: #586e75; } /* Name.Attribute */ + .nb { color: #b58900; } /* Name.Builtin */ + .nc { color: #268bd2; } /* Name.Class */ + .no { color: #cb4b16; } /* Name.Constant */ + .nd { color: #268bd2; } /* Name.Decorator */ + .ni { color: #cb4b16; } /* Name.Entity */ + .ne { color: #cb4b16; } /* Name.Exception */ + .nf { color: #268bd2; } /* Name.Function */ + .nl { color: #586e75; } /* Name.Label */ + .nn { color: #586e75; } /* Name.Namespace */ + .nx { color: #586e75; } /* Name.Other */ + .py { color: #586e75; } /* Name.Property */ + .nt { color: #268bd2; } /* Name.Tag */ + .nv { color: #268bd2; } /* Name.Variable */ + .ow { color: #859900; } /* Operator.Word */ + .w { color: #586e75; } /* Text.Whitespace */ + .mf { color: #2aa198; } /* Literal.Number.Float */ + .mh { color: #2aa198; } /* Literal.Number.Hex */ + .mi { color: #2aa198; } /* Literal.Number.Integer */ + .mo { color: #2aa198; } /* Literal.Number.Oct */ + .sb { color: #93a1a1; } /* Literal.String.Backtick */ + .sc { color: #2aa198; } /* Literal.String.Char */ + .sd { color: #586e75; } /* Literal.String.Doc */ + .s2 { color: #2aa198; } /* Literal.String.Double */ + .se { color: #cb4b16; } /* Literal.String.Escape */ + .sh { color: #586e75; } /* Literal.String.Heredoc */ + .si { color: #2aa198; } /* Literal.String.Interpol */ + .sx { color: #2aa198; } /* Literal.String.Other */ + .sr { color: #dc322f; } /* Literal.String.Regex */ + .s1 { color: #2aa198; } /* Literal.String.Single */ + .ss { color: #2aa198; } /* Literal.String.Symbol */ + .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ + .vc { color: #268bd2; } /* Name.Variable.Class */ + .vg { color: #268bd2; } /* Name.Variable.Global */ + .vi { color: #268bd2; } /* Name.Variable.Instance */ + .il { color: #2aa198; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 36a80a916b2..6f31a5235c0 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -86,7 +86,7 @@ background-color: #fafe3d !important; } - .hll { background-color: #f8f8f8 } + .hll { background-color: #f8f8f8; } .c { color: #998; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } .k { font-weight: bold; } diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss index 5bfe9bcb443..8d1a6020ca4 100644 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -78,7 +78,7 @@ span.highlight_word { background-color: #fafe3d !important; } -.hll { background-color: #f8f8f8 } +.hll { background-color: #f8f8f8; } .c { color: #998; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } .k { font-weight: bold; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index fc12964872d..ced8c4a9907 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -2,22 +2,28 @@ img { max-width: 100%; height: auto; } + p.details { font-style: italic; - color: #777 + color: #777; } + .footer > p { font-size: small; - color: #777 + color: #777; } + pre.commit-message { white-space: pre-wrap; } + .file-stats > a { text-decoration: none; + > .new-file { color: #090; } + > .deleted-file { color: #b00; } diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 8f71381f5c4..140d589024b 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -22,7 +22,7 @@ .admin-filter form { .select2-container { - width: 100% + width: 100%; } .controls { @@ -31,7 +31,7 @@ .form-actions { padding-left: 130px; - background: #fff + background: #fff; } .visibility-levels { @@ -106,26 +106,33 @@ .table { table-layout: fixed; } + .subheading { padding-bottom: $gl-padding; } + .message { word-wrap: break-word; } + .btn { white-space: normal; padding: $gl-btn-padding; } + th { width: 15%; + &.wide { width: 55%; } } + @media (max-width: $screen-sm-max) { th { width: 100%; } + td { width: 100%; float: left; @@ -137,6 +144,7 @@ margin-left: $btn-side-margin; margin-top: 3px; } + span { font-size: 19px; } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ecc5b24e360..6e81c12aa55 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -162,6 +162,10 @@ lex list-style: none; overflow-y: scroll; overflow-x: hidden; + + &.is-smaller { + height: calc(100% - 185px); + } } .board-list-loading { @@ -233,3 +237,31 @@ lex margin-right: 5px; } } + +.board-new-issue-form { + margin: 5px; +} + +.board-issue-count-holder { + margin-top: -3px; + + .btn { + line-height: 12px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} + +.board-issue-count { + padding-right: 10px; + padding-left: 10px; + line-height: 21px; + border-radius: $border-radius-base; + border: 1px solid $border-color; + + &.has-btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-width: 1px 0 1px 1px; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 194a39a8377..2fbf0cf34bf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -137,6 +137,7 @@ .retry-link { color: $gl-link-color; + &:hover { text-decoration: underline; } @@ -218,6 +219,7 @@ .build-detail-row { margin-bottom: 5px; + &:last-of-type { margin-bottom: 0; } @@ -233,3 +235,9 @@ right: 0; margin-top: -17px; } + +@media (min-width: $screen-md-min) { + .sub-nav.build { + width: calc(100% + #{$gutter_width}); + } +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 53ec0002afe..264e7e01a34 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -51,6 +51,7 @@ margin-left: 4px; } } + .commit-committer-link, .commit-author-link { color: $gl-gray; @@ -108,21 +109,25 @@ line-height: 20px; } } + .new-file { a { color: $gl-text-green; } } + .renamed-file { a { color: $gl-text-orange; } } + .deleted-file { a { color: $gl-text-red; } } + .edit-file { a { color: $gl-text-color; @@ -158,6 +163,7 @@ position: absolute; z-index: 1; } + > textarea { background-color: rgba(0, 0, 0, 0.0); font-family: inherit; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index dc57a837155..2b5621e20d6 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -161,6 +161,7 @@ .branch-commit { color: $gl-gray; + .commit-id, .commit-row-message { color: $gl-gray; } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 778471a34d7..d732008de3d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -50,7 +50,7 @@ .bordered-box { border: 1px solid $border-color; - @include border-radius($border-radius-default); + border-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 42928ee279c..76225ed8d06 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -5,6 +5,7 @@ background: $background-color; border-top-left-radius: 0; } + border-top-left-radius: 0; } } @@ -17,6 +18,7 @@ float: left; @extend .col-md-2; } + .btn { margin-left: 5px; float: left; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b8ef76cc74e..bdc82a8f0f5 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -33,6 +33,19 @@ font-size: smaller; } } + + .file-title { + cursor: pointer; + + &:hover { + background-color: $dark-background-color; + } + + .diff-toggle-caret { + padding-right: 6px; + } + } + .diff-content { overflow: auto; overflow-y: hidden; @@ -123,15 +136,18 @@ max-width: 50px; width: 35px; @include user-select(none); + a { float: left; width: 35px; font-weight: normal; + &:hover { text-decoration: underline; } } } + .line_content { display: block; margin: 0; @@ -151,10 +167,12 @@ white-space: pre-wrap; } } + .image { background: #ddd; text-align: center; padding: 30px; + .wrap { display: inline-block; } @@ -163,6 +181,7 @@ display: inline-block; background-color: #fff; line-height: 0; + img { border: 1px solid #fff; background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%), @@ -171,6 +190,7 @@ background-position: 0 0, 5px 5px; max-width: 100%; } + &.deleted { border: 1px solid $deleted; } @@ -179,6 +199,7 @@ border: 1px solid $added; } } + .image-info { font-size: 12px; margin: 5px 0 0; @@ -193,6 +214,7 @@ margin: auto; position: relative; } + .swipe-wrap { overflow: hidden; border-left: 1px solid #999; @@ -201,10 +223,12 @@ top: 13px; right: 7px; } + .frame { top: 0; right: 0; position: absolute; + &.deleted { margin: 0; display: block; @@ -212,6 +236,7 @@ right: 7px; } } + .swipe-bar { display: block; height: 100%; @@ -219,14 +244,17 @@ z-index: 100; position: absolute; cursor: pointer; + &:hover { .top-handle { background-position: -15px 3px; } + .bottom-handle { background-position: -15px -11px; } } + .top-handle { display: block; height: 14px; @@ -235,6 +263,7 @@ top: 0; background: image-url('swipemode_sprites.gif') 0 3px no-repeat; } + .bottom-handle { display: block; height: 14px; @@ -252,12 +281,14 @@ margin: auto; position: relative; } + .frame.added, .frame.deleted { position: absolute; display: block; top: 0; left: 0; } + .controls { display: block; height: 14px; @@ -311,6 +342,7 @@ } //.view.onion-skin } + .view-modes { padding: 10px; text-align: center; @@ -328,19 +360,24 @@ border-left: 1px solid #c1c1c1; padding: 0 12px 0 16px; cursor: pointer; + &:first-child { border-left: none; } + &:hover { text-decoration: underline; } + &.active { &:hover { text-decoration: none; } + cursor: default; color: #333; } + &.disabled { display: none; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index e1304335271..029dabd2138 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,7 +1,7 @@ .file-editor { #editor { border: none; - @include border-radius(0); + border-radius: 0; height: 500px; margin: 0; padding: 0; @@ -15,6 +15,7 @@ .cancel-btn { color: #b94a48; + &:hover { color: #b94a48; } @@ -70,16 +71,20 @@ .soft-wrap-toggle { margin: 0 $btn-side-margin; + .soft-wrap { display: block; } + .no-wrap { display: none; } + &.soft-wrap-active { .soft-wrap { display: none; } + .no-wrap { display: block; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 2e14bdb0eb3..9d3492abfb5 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,4 +1,15 @@ +.environments-container, +.deployments-container { + width: 100%; + overflow: auto; +} + .environments { + .deployment-column { + .avatar { + float: none; + } + } .commit-title { margin: 0; @@ -9,6 +20,7 @@ width: 12px; } + .external-url, .dropdown-new { color: $table-text-gray; } @@ -21,6 +33,7 @@ } } + .build-link, .branch-name { color: $gl-dark-link-color; } @@ -32,13 +45,31 @@ font-size: 14px; } } + + .deployment { + .build-column { + + .build-link { + color: $gl-dark-link-color; + } + + .avatar { + float: none; + } + } + } } -.table.builds.environments { - min-width: 500px; +.table.ci-table.environments { .icon-container { width: 20px; text-align: center; } + + .branch-commit { + .commit-id { + margin-right: 0; + } + } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 1d00da1266c..5d9a76dac05 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -78,6 +78,7 @@ margin-bottom: 0; } } + .event-note-icon { color: #777; float: left; @@ -86,21 +87,23 @@ margin-right: 5px; } } + .event_icon { position: relative; float: right; border: 1px solid #eee; padding: 5px; - @include border-radius(5px); + border-radius: 5px; background: $gray-light; margin-left: 10px; top: -6px; + img { width: 20px; } } - &:last-child { border: none } + &:last-child { border: none; } .event_commits { li { @@ -109,6 +112,7 @@ padding: 3px; padding-left: 0; border: none; + .commit-row-title { font-size: $gl-font-size; } @@ -117,6 +121,7 @@ &.commits-stat { display: block; padding: 0 3px 0 0; + &:hover { background: none; } @@ -158,6 +163,7 @@ overflow: visible; max-width: 100%; } + .avatar { display: none; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 185ce970e71..ee2a398f031 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,17 +1,3 @@ -.member-search-form { - float: left; - - input[type='search'] { - width: 225px; - vertical-align: bottom; - - @media (max-width: $screen-xs-max) { - width: 100px; - vertical-align: bottom; - } - } -} - .milestone-row { @include str-truncated(90%); } @@ -48,6 +34,7 @@ .group-right-buttons { position: absolute; right: 16px; + .btn { @include btn-gray; padding: 3px 10px; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 00ab42bec5c..a48b4c65db8 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -23,28 +23,28 @@ color: #555; tbody:first-child tr:first-child { - padding-top: 0 + padding-top: 0; } th { padding-top: 15px; line-height: 1.5; color: #333; - text-align: left + text-align: left; } td { padding-top: 3px; padding-bottom: 3px; vertical-align: top; - line-height: 20px + line-height: 20px; } .shortcut { padding-right: 10px; color: #999; text-align: right; - white-space: nowrap + white-space: nowrap; } .key { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 41079b6eeb5..230b927a17d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -27,6 +27,7 @@ margin-right: 5px; margin-bottom: 5px; display: inline-block; + .color-label { padding: 6px 10px; } @@ -128,7 +129,7 @@ } .selectbox { - display: none + display: none; } .btn-clipboard { @@ -199,7 +200,7 @@ display: none; /* Small devices (tablets, 768px and up) */ @media (min-width: $screen-sm-min) { - display: block + display: block; } width: $sidebar_collapsed_width; @@ -276,7 +277,7 @@ } &.btn-primary { - @extend .btn-primary + @extend .btn-primary; } } @@ -400,6 +401,7 @@ .js-issuable-selector { width: 100%; } + @media (max-width: $screen-sm-max) { margin-bottom: $gl-padding; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3ac34cbc829..623da67a239 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -37,6 +37,7 @@ ul.related-merge-requests > li { display: -ms-flexbox; display: -webkit-flex; display: flex; + .merge-request-id { flex-shrink: 0; } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 38c7cd98e41..9bac6d46355 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -1,7 +1,8 @@ .suggest-colors { margin-top: 5px; + a { - @include border-radius(4px); + border-radius: 4px; width: 30px; height: 30px; display: inline-block; @@ -17,7 +18,7 @@ overflow: hidden; a { - @include border-radius(0); + border-radius: 0; width: (100% / 7); margin-right: 0; margin-bottom: -5px; @@ -59,6 +60,13 @@ width: 200px; margin-bottom: 0; } + + .label { + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + max-width: 100%; + } } .label-description { diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 6926448519e..8290519dc25 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -3,6 +3,7 @@ font-size: 19px; color: red; } + .correct-syntax { font-size: 19px; color: #47a447; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 403171d4532..4c0c0839fe2 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -17,6 +17,7 @@ line-height: 1.5; p { + font-size: 18px; color: #888; } @@ -36,10 +37,14 @@ } } - .login-box { - background: #fafafa; - border-radius: 10px; - box-shadow: 0 0 2px #ccc; + p { + font-size: 13px; + } + + .login-box, .omniauth-container { + box-shadow: 0 0 0 1px $border-color; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 2px; padding: 15px; .login-heading h3 { @@ -58,42 +63,127 @@ a.forgot { float: right; - padding-top: 6px + padding-top: 6px; } .nav .active a { background: transparent; } - } - .form-control { - font-size: 14px; - padding: 10px 8px; - width: 100%; - height: auto; - - &.top { - @include border-radius(5px 5px 0 0); - margin-bottom: 0; + // Styles the glowing border of focused input for username async validation + .login-body { + font-size: 13px; + + + input + p { + margin-top: 5px; + } + + .gl-field-success-outline { + border: 1px solid $green-normal; + + &:focus { + box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; + border: 0 none; + } + } + + .gl-field-error-outline { + border: 1px solid $red-normal; + + &:focus { + box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); + border: 0 none; + } + } + + .username .validation-success, + .gl-field-success-message { + color: $green-normal; + } + + .username .validation-error, + .gl-field-error-message { + color: $red-normal; + } + + .gl-field-hint { + color: $gl-text-color; + } + } + } - &.bottom { - @include border-radius(0 0 5px 5px); - border-top: 0; - margin-bottom: 20px; + .omniauth-container { + p { + margin: 0; } + } + + .new-session-tabs { + display: -webkit-flex; + display: flex; + box-shadow: 0 0 0 1px $border-color; + border-top-right-radius: 2px; + border-top-left-radius: 2px; + + li { + flex: 1; + text-align: center; + + &:last-of-type { + border-left: 1px solid $border-color; + } + + &:not(.active) { + background-color: $gray-light; + } + + a { + width: 100%; + font-size: 18px; - &.middle { - border-top: 0; - margin-bottom: 0; - @include border-radius(0); + &:hover { + border: 1px solid transparent; + } + } + + &.active { + border-bottom: 1px solid $border-color; + + a { + border: none; + border-bottom: 2px solid $link-underline-blue; + color: $black; + + &:hover { + border-bottom: 2px solid $link-underline-blue; + } + } + } } + } + + .form-control { &:active, &:focus { background-color: #fff; } } + label { + font-weight: normal; + } + + .submit-container { + margin-top: 16px; + } + + input[type="submit"] { + @extend .btn-block; + margin-bottom: 0; + } + .devise-errors { h2 { margin-top: 0; @@ -101,14 +191,6 @@ color: #a00; } } - - .remember-me { - margin-top: -10px; - - label { - font-weight: normal; - } - } } @media (max-width: $screen-xs-max) { @@ -127,3 +209,35 @@ height: 32px; } } + +.devise-layout-html { + margin: 0; + padding: 0; + height: 100%; +} + +// Fixes footer container to bottom of viewport +.devise-layout-html body { + // offset height of fixed header + 1 to avoid scroll + height: calc(100% - 51px); + margin: 0; + padding: 0; + + .page-wrap { + min-height: 100%; + position: relative; + } + + .footer-container, hr.footer-fixed { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 40px; + background: $white-light; + } + + .navless-container { + padding: 65px; // height of footer + bottom padding of email confirmation link + } +} diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss new file mode 100644 index 00000000000..756efa9c7fa --- /dev/null +++ b/app/assets/stylesheets/pages/members.scss @@ -0,0 +1,98 @@ +.project-members-title { + padding-bottom: 10px; + border-bottom: 1px solid $border-color; +} + +.member { + .list-item-name { + @media (min-width: $screen-sm-min) { + float: left; + width: 50%; + } + + strong { + font-weight: 600; + } + } + + .controls { + @media (min-width: $screen-sm-min) { + display: -webkit-flex; + display: flex; + width: 400px; + max-width: 50%; + } + } + + .form-horizontal { + margin-top: 5px; + + @media (min-width: $screen-sm-min) { + display: -webkit-flex; + display: flex; + width: 100%; + margin-top: 3px; + } + } + + .btn-remove { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: auto; + } + } +} + +.member-form-control { + @media (max-width: $screen-xs-max) { + padding: 5px 0; + margin-left: 0; + margin-right: 0; + } + + @media (min-width: $screen-sm-min) { + width: 50%; + } +} + +.member-access-text { + margin-left: auto; + line-height: 43px; +} + +.member.existing-title { + @media (min-width: $screen-sm-min) { + float: left; + } +} + +.member-search-form { + position: relative; + + @media (min-width: $screen-sm-min) { + float: right; + } + + .form-control { + width: 100%; + padding-right: 35px; + + @media (min-width: $screen-sm-min) { + width: 350px; + } + } +} + +.member-search-btn { + position: absolute; + right: 0; + top: 0; + height: 35px; + padding-left: 10px; + padding-right: 10px; + color: $gray-darkest; + background: transparent; + border: 0; + outline: 0; +} diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 5ec660799e3..49013d7cac9 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -131,6 +131,7 @@ $colors: ( } } } + &.head { background-color: map-get($colors, #{$color}_header_head_neutral); border-color: map-get($colors, #{$color}_header_head_neutral); @@ -174,6 +175,7 @@ $colors: ( background-color: map-get($colors, #{$color}_line_not_chosen); } } + &.head { background-color: map-get($colors, #{$color}_line_head_neutral); diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f2c2e06ad3e..f14f13494e0 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -6,10 +6,11 @@ background: $background-color; color: $gl-gray; border: 1px solid $border-color; - @include border-radius(2px); + border-radius: 2px; form { margin-bottom: 0; + .clearfix { margin-bottom: 0; } @@ -46,6 +47,7 @@ &.right { float: right; + a { color: $gl-gray; } @@ -121,6 +123,10 @@ color: #5c5d5e; } + .js-deployment-link { + display: inline-block; + } + .mr-widget-body { h4 { font-weight: 600; @@ -197,6 +203,7 @@ padding-top: 2px; padding-bottom: 2px; list-style: none; + &:hover { background: none; } @@ -213,6 +220,19 @@ word-break: break-all; } +.commits-empty { + text-align: center; + + h4 { + padding-top: 20px; + padding-bottom: 10px; + } + + svg { + width: 230px; + } +} + .mr-list { .merge-request { padding: 10px 15px; @@ -273,12 +293,6 @@ line-height: 31px; } -.builds { - .table-holder { - overflow-x: auto; - } -} - .panel-new-merge-request { .panel-heading { padding: 5px 10px; @@ -366,7 +380,7 @@ } .table-holder { - .builds { + .ci-table { th { background-color: $white-light; @@ -398,8 +412,12 @@ padding: 16px; } + .content-block { + border-top: 1px solid $border-color; + padding: $gl-padding-top $gl-padding; + } + .comments-disabled-notif { - padding: 10px 16px; .btn { margin-left: 5px; } @@ -410,10 +428,6 @@ margin: 0 7px; } - .comments-disabled-notif { - border-top: 1px solid $border-color; - } - .dropdown-title { color: $gl-text-color; } @@ -430,3 +444,12 @@ margin-bottom: 20px; } } + +.merge-request-tabs { + background-color: #fff; + + &.affix { + top: 100px; + z-index: 9; + } +} diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 8c2ba3ed58c..dd6d1783667 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -59,6 +59,7 @@ color: $gl-placeholder-color; margin-right: 5px; } + .avatar { float: none; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index bd875b9823f..17f28959414 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -11,6 +11,7 @@ filter: alpha(opacity=100); } } + .diff-file, .discussion { .new-note { @@ -194,6 +195,7 @@ min-height: 140px; max-height: 500px; } + .note-form-actions { background: transparent; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 54124a3d658..fffcdc812a7 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -147,9 +147,18 @@ ul.notes { // Diff code in discussion view .discussion-body .diff-file { + .file-title { + cursor: default; + + &:hover { + background-color: $gray-light; + } + } + .diff-header > span { margin-right: 10px; } + .line_content { white-space: pre-wrap; } @@ -334,7 +343,7 @@ ul.notes { .add-diff-note { margin-top: -4px; - @include border-radius(40px); + border-radius: 40px; background: #fff; padding: 4px; font-size: 16px; @@ -345,6 +354,7 @@ ul.notes { width: 32px; // "hide" it by default display: none; + &:hover { background: $gl-info; color: #fff; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 68fc6da6c1b..7b71876b822 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -20,7 +20,7 @@ margin: 4px; } - .table.builds { + .table.ci-table { min-width: 1200px; .branch-commit { @@ -44,13 +44,20 @@ overflow: auto; } -.table.builds { +.table.ci-table { min-width: 900px; &.pipeline { min-width: 650px; } + &.builds-page { + + tr { + height: 71px; + } + } + tr { th { padding: 16px 8px; @@ -176,7 +183,7 @@ &::after { content: ''; width: 8px; - position: absolute;; + position: absolute; right: -7px; bottom: 8px; border-bottom: 2px solid $border-color; @@ -229,9 +236,12 @@ .fa { color: $table-text-gray; - margin-right: 6px; font-size: 14px; } + + svg, .fa { + margin-right: 0; + } } .btn-remove { @@ -272,18 +282,8 @@ .toggle-pipeline-btn { background-color: $gray-dark; - .caret { - border-top: none; - border-bottom: 4px solid; - } - &.graph-collapsed { background-color: $white-light; - - .caret { - border-bottom: none; - border-top: 4px solid; - } } } @@ -310,16 +310,41 @@ .stage-column { display: inline-block; vertical-align: top; - margin-right: 65px; + + &:not(:last-child) { + margin-right: 44px; + } + + &.left-margin { + &:not(:first-child) { + margin-left: 44px; + + .left-connector { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -48px; + border-top: 2px solid $border-color; + width: 48px; + height: 1px; + } + } + } + } + + &.no-margin { + margin: 0; + } li { list-style: none; } .stage-name { - margin-bottom: 15px; + margin: 0 0 15px 10px; font-weight: bold; - width: 150px; + width: 176px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -328,17 +353,24 @@ .build { border: 1px solid $border-color; position: relative; - padding: 6px 10px; + padding: 7px 10px 8px; border-radius: 30px; - width: 150px; + width: 186px; margin-bottom: 10px; + &:hover { + background-color: $gray-lighter; + + .dropdown-menu-toggle { + background-color: transparent; + } + } + &.playable { - background-color: $gray-light; svg { - height: 12px; - width: 12px; + height: 13px; + width: 20px; position: relative; top: 1px; @@ -349,10 +381,20 @@ } .build-content { - width: 130px; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + width: 164px; + + .ci-status-icon { + svg { + height: 20px; + width: 20px; + } + } .ci-status-text { - width: 110px; + width: 135px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -363,44 +405,56 @@ } a { - color: $layout-link-gray; + color: $gl-text-color-light; text-decoration: none; - - &:hover { - .ci-status-text { - text-decoration: underline; - } - } } .dropdown-menu-toggle { border: none; width: auto; padding: 0; - color: $layout-link-gray; + color: $gl-text-color-light; + flex-grow: 1; .ci-status-text { - width: 80px; + max-width: 112px; + width: auto; } } .grouped-pipeline-dropdown { padding: 8px 0; - width: 200px; + width: 186px; left: auto; - right: -214px; + right: -197px; top: -9px; - max-height: 245px; - overflow-y: scroll; - a:hover { - .ci-status-text { - text-decoration: none; + ul { + max-height: 245px; + overflow: auto; + } + + a { + color: $gl-text-color; + padding: 7px 8px 8px; + + &:hover { + background-color: $blue-light-transparent; + border-radius: 3px; + + .ci-status-text { + text-decoration: none; + } } } + svg { + width: 14px; + height: 14px; + } + .ci-status-text { - width: 145px; + width: 112px; } .arrow { @@ -433,9 +487,10 @@ } .badge { - background-color: $gray-dark; - color: $layout-link-gray; + background-color: $gray-darker; + color: $gl-text-color-light; font-weight: normal; + margin-left: $btn-xs-side-margin; } } @@ -449,10 +504,10 @@ &::after { content: ''; position: absolute; - top: 50%; - right: -69px; + top: 48%; + right: -48px; border-top: 2px solid $border-color; - width: 69px; + width: 48px; height: 1px; } } @@ -461,25 +516,25 @@ &:not(:first-child) { &::after, &::before { content: ''; - top: -47px; + top: -49px; position: absolute; border-bottom: 2px solid $border-color; - width: 20px; - height: 65px; + width: 25px; + height: 69px; } // Right connecting curves &::after { - right: -20px; + right: -25px; border-right: 2px solid $border-color; - border-radius: 0 0 15px; + border-radius: 0 0 20px; } // Left connecting curves &::before { - left: -20px; + left: -25px; border-left: 2px solid $border-color; - border-radius: 0 0 0 15px; + border-radius: 0 0 0 20px; } } @@ -487,8 +542,9 @@ &:nth-child(2) { &::after, &::before { height: 29px; - top: -10px; + top: -9px; } + .curve { display: block; } @@ -545,20 +601,20 @@ width: 21px; height: 25px; position: absolute; - top: -29px; + top: -32px; border-top: 2px solid $border-color; } &::after { - left: -39px; + left: -44px; border-right: 2px solid $border-color; - border-radius: 0 15px; + border-radius: 0 20px; } &::before { - right: -39px; + right: -44px; border-left: 2px solid $border-color; - border-radius: 15px 0 0; + border-radius: 20px 0 0; } } } @@ -574,19 +630,31 @@ } } -.pipelines.tab-pane { +.tab-pane { - .content-list.pipelines { - overflow: auto; - } + &.pipelines { - .stage { - max-width: 100px; - width: 100px; + .content-list.pipelines { + overflow: auto; + } + + .stage { + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } } - .pipeline-actions { - min-width: initial; + &.builds { + + .ci-table { + tr { + height: 71px; + } + } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 0fcdaf94a21..ed80d2beec2 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -94,7 +94,7 @@ .profile-user-bio { // Limits the width of the user bio for readability. max-width: 600px; - margin: 15px auto 0; + margin: 10px auto; padding: 0 16px; } @@ -213,29 +213,22 @@ } .user-profile { + .cover-controls a { margin-left: 5px; } + .profile-header { margin: 0 auto; + .avatar-holder { width: 90px; - display: inline-block; - } - .user-info { - display: inline-block; - text-align: left; - vertical-align: middle; - margin-left: 15px; - .handle { - color: $gl-gray-light; - } - .member-date { - margin-bottom: 4px; - } + margin: 0 auto 10px; } } + @media (max-width: $screen-xs-max) { + .cover-block { padding-top: 20px; } @@ -250,6 +243,7 @@ .btn { -webkit-flex-grow: 1; flex-grow: 1; + &:first-child { margin-left: 0; } @@ -258,10 +252,6 @@ } } -.user-profile-nav { - margin-top: 15px; -} - table.u2f-registrations { th:not(:last-child), td:not(:last-child) { border-right: solid 1px transparent; diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index e5859fe7384..f8da0983b77 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -4,7 +4,7 @@ text-align: center; .preview { - @include border-radius(4px); + border-radius: 4px; height: 80px; margin-bottom: 10px; @@ -47,7 +47,7 @@ width: 160px; img { - @include border-radius(4px); + border-radius: 4px; max-width: 100%; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 87548dcb590..d30f02340b9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -17,34 +17,43 @@ &.features .control-label { font-weight: normal; } + .form-group { margin-bottom: 5px; } + &> .form-group { padding-left: 0; } } + .help-block { margin-bottom: 10px; } + .project-path { padding-right: 0; + .form-control { border-radius: $border-radius-base; } } + .input-group > div { &:last-child { padding-right: 0; } } + @media (max-width: $screen-xs-max) { .input-group > div { margin-bottom: 14px; + &:last-child { margin-bottom: 0; } } + fieldset > .form-group:first-child { padding-right: 0; } @@ -56,6 +65,7 @@ border-radius: 3px; border: 1px solid #e5e5e5; } + &+ .select2 a { border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -201,6 +211,7 @@ pointer-events: none; } } + .count { @include btn-gray; display: inline-block; @@ -354,35 +365,40 @@ a.deploy-project-label { justify-content: flex-start; .fork-thumbnail { - @include border-radius($border-radius-base); + border-radius: $border-radius-base; background-color: $white-light; border: 1px solid $border-white-light; height: 202px; margin: $gl-padding; text-align: center; width: 169px; + &:hover, &.forked { background-color: $row-hover; border-color: $row-hover-border; } + .no-avatar { width: 100px; height: 100px; background-color: $gray-light; border: 1px solid $gray-dark; margin: 0 auto; - @include border-radius(50%); + border-radius: 50%; + i { font-size: 100px; color: $gray-dark; } } + a { display: block; width: 100%; height: 100%; padding-top: $gl-padding; color: $gl-gray; + .caption { min-height: 30px; padding: $gl-padding 0; @@ -390,7 +406,7 @@ a.deploy-project-label { } img { - @include border-radius(50%); + border-radius: 50%; max-width: 100px; } } @@ -496,7 +512,7 @@ pre.light-well { } .light-well { - @include border-radius (2px); + border-radius: 2px; color: #5b6169; font-size: 13px; @@ -644,6 +660,7 @@ pre.light-well { .clone-options { display: table-cell; + a.btn { width: 100%; } @@ -832,6 +849,7 @@ pre.light-well { .form-control { min-width: 100px; } + .select2-choice { border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index eec22c5dc96..7b3878c91df 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -6,6 +6,7 @@ &.runner-state-shared { background: #32b186; } + &.runner-state-specific { background: #3498db; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 0ee7ceecae5..f1d53c7b8bc 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -4,7 +4,7 @@ margin-right: 10px; border: 1px solid #eee; white-space: nowrap; - @include border-radius(4px); + border-radius: 4px; &:hover { text-decoration: none; @@ -65,6 +65,7 @@ .ci-status-icon-success { color: $gl-success; } + .ci-status-icon-failed { color: $gl-danger; } @@ -77,6 +78,7 @@ .ci-status-icon-running { color: $blue-normal; } + .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found, diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 41ad10f07bd..99c0f6362d0 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -5,6 +5,7 @@ .file-finder { width: 50%; + .file-finder-input { width: 95%; display: inline-block; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index c9846103762..3fa7fa3d7e3 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -23,15 +23,19 @@ .term-bold { font-weight: bold; } + .term-italic { font-style: italic; } + .term-conceal { visibility: hidden; } + .term-underline { text-decoration: underline; } + .term-cross { text-decoration: line-through; } @@ -39,48 +43,63 @@ .term-fg-black { color: $black; } + .term-fg-red { color: $red; } + .term-fg-green { color: $green; } + .term-fg-yellow { color: $yellow; } + .term-fg-blue { color: $blue; } + .term-fg-magenta { color: $magenta; } + .term-fg-cyan { color: $cyan; } + .term-fg-white { color: $white; } + .term-fg-l-black { color: $l-black; } + .term-fg-l-red { color: $l-red; } + .term-fg-l-green { color: $l-green; } + .term-fg-l-yellow { color: $l-yellow; } + .term-fg-l-blue { color: $l-blue; } + .term-fg-l-magenta { color: $l-magenta; } + .term-fg-l-cyan { color: $l-cyan; } + .term-fg-l-white { color: $l-white; } @@ -88,818 +107,1087 @@ .term-bg-black { background-color: $black; } + .term-bg-red { background-color: $red; } + .term-bg-green { background-color: $green; } + .term-bg-yellow { background-color: $yellow; } + .term-bg-blue { background-color: $blue; } + .term-bg-magenta { background-color: $magenta; } + .term-bg-cyan { background-color: $cyan; } + .term-bg-white { background-color: $white; } + .term-bg-l-black { background-color: $l-black; } + .term-bg-l-red { background-color: $l-red; } + .term-bg-l-green { background-color: $l-green; } + .term-bg-l-yellow { background-color: $l-yellow; } + .term-bg-l-blue { background-color: $l-blue; } + .term-bg-l-magenta { background-color: $l-magenta; } + .term-bg-l-cyan { background-color: $l-cyan; } + .term-bg-l-white { background-color: $l-white; } - .xterm-fg-0 { color: #000; } + .xterm-fg-1 { color: #800000; } + .xterm-fg-2 { color: #008000; } + .xterm-fg-3 { color: #808000; } + .xterm-fg-4 { color: #000080; } + .xterm-fg-5 { color: #800080; } + .xterm-fg-6 { color: #008080; } + .xterm-fg-7 { color: #c0c0c0; } + .xterm-fg-8 { color: #808080; } + .xterm-fg-9 { color: #f00; } + .xterm-fg-10 { color: #0f0; } + .xterm-fg-11 { color: #ff0; } + .xterm-fg-12 { color: #00f; } + .xterm-fg-13 { color: #f0f; } + .xterm-fg-14 { color: #0ff; } + .xterm-fg-15 { color: #fff; } + .xterm-fg-16 { color: #000; } + .xterm-fg-17 { color: #00005f; } + .xterm-fg-18 { color: #000087; } + .xterm-fg-19 { color: #0000af; } + .xterm-fg-20 { color: #0000d7; } + .xterm-fg-21 { color: #00f; } + .xterm-fg-22 { color: #005f00; } + .xterm-fg-23 { color: #005f5f; } + .xterm-fg-24 { color: #005f87; } + .xterm-fg-25 { color: #005faf; } + .xterm-fg-26 { color: #005fd7; } + .xterm-fg-27 { color: #005fff; } + .xterm-fg-28 { color: #008700; } + .xterm-fg-29 { color: #00875f; } + .xterm-fg-30 { color: #008787; } + .xterm-fg-31 { color: #0087af; } + .xterm-fg-32 { color: #0087d7; } + .xterm-fg-33 { color: #0087ff; } + .xterm-fg-34 { color: #00af00; } + .xterm-fg-35 { color: #00af5f; } + .xterm-fg-36 { color: #00af87; } + .xterm-fg-37 { color: #00afaf; } + .xterm-fg-38 { color: #00afd7; } + .xterm-fg-39 { color: #00afff; } + .xterm-fg-40 { color: #00d700; } + .xterm-fg-41 { color: #00d75f; } + .xterm-fg-42 { color: #00d787; } + .xterm-fg-43 { color: #00d7af; } + .xterm-fg-44 { color: #00d7d7; } + .xterm-fg-45 { color: #00d7ff; } + .xterm-fg-46 { color: #0f0; } + .xterm-fg-47 { color: #00ff5f; } + .xterm-fg-48 { color: #00ff87; } + .xterm-fg-49 { color: #00ffaf; } + .xterm-fg-50 { color: #00ffd7; } + .xterm-fg-51 { color: #0ff; } + .xterm-fg-52 { color: #5f0000; } + .xterm-fg-53 { color: #5f005f; } + .xterm-fg-54 { color: #5f0087; } + .xterm-fg-55 { color: #5f00af; } + .xterm-fg-56 { color: #5f00d7; } + .xterm-fg-57 { color: #5f00ff; } + .xterm-fg-58 { color: #5f5f00; } + .xterm-fg-59 { color: #5f5f5f; } + .xterm-fg-60 { color: #5f5f87; } + .xterm-fg-61 { color: #5f5faf; } + .xterm-fg-62 { color: #5f5fd7; } + .xterm-fg-63 { color: #5f5fff; } + .xterm-fg-64 { color: #5f8700; } + .xterm-fg-65 { color: #5f875f; } + .xterm-fg-66 { color: #5f8787; } + .xterm-fg-67 { color: #5f87af; } + .xterm-fg-68 { color: #5f87d7; } + .xterm-fg-69 { color: #5f87ff; } + .xterm-fg-70 { color: #5faf00; } + .xterm-fg-71 { color: #5faf5f; } + .xterm-fg-72 { color: #5faf87; } + .xterm-fg-73 { color: #5fafaf; } + .xterm-fg-74 { color: #5fafd7; } + .xterm-fg-75 { color: #5fafff; } + .xterm-fg-76 { color: #5fd700; } + .xterm-fg-77 { color: #5fd75f; } + .xterm-fg-78 { color: #5fd787; } + .xterm-fg-79 { color: #5fd7af; } + .xterm-fg-80 { color: #5fd7d7; } + .xterm-fg-81 { color: #5fd7ff; } + .xterm-fg-82 { color: #5fff00; } + .xterm-fg-83 { color: #5fff5f; } + .xterm-fg-84 { color: #5fff87; } + .xterm-fg-85 { color: #5fffaf; } + .xterm-fg-86 { color: #5fffd7; } + .xterm-fg-87 { color: #5fffff; } + .xterm-fg-88 { color: #870000; } + .xterm-fg-89 { color: #87005f; } + .xterm-fg-90 { color: #870087; } + .xterm-fg-91 { color: #8700af; } + .xterm-fg-92 { color: #8700d7; } + .xterm-fg-93 { color: #8700ff; } + .xterm-fg-94 { color: #875f00; } + .xterm-fg-95 { color: #875f5f; } + .xterm-fg-96 { color: #875f87; } + .xterm-fg-97 { color: #875faf; } + .xterm-fg-98 { color: #875fd7; } + .xterm-fg-99 { color: #875fff; } + .xterm-fg-100 { color: #878700; } + .xterm-fg-101 { color: #87875f; } + .xterm-fg-102 { color: #878787; } + .xterm-fg-103 { color: #8787af; } + .xterm-fg-104 { color: #8787d7; } + .xterm-fg-105 { color: #8787ff; } + .xterm-fg-106 { color: #87af00; } + .xterm-fg-107 { color: #87af5f; } + .xterm-fg-108 { color: #87af87; } + .xterm-fg-109 { color: #87afaf; } + .xterm-fg-110 { color: #87afd7; } + .xterm-fg-111 { color: #87afff; } + .xterm-fg-112 { color: #87d700; } + .xterm-fg-113 { color: #87d75f; } + .xterm-fg-114 { color: #87d787; } + .xterm-fg-115 { color: #87d7af; } + .xterm-fg-116 { color: #87d7d7; } + .xterm-fg-117 { color: #87d7ff; } + .xterm-fg-118 { color: #87ff00; } + .xterm-fg-119 { color: #87ff5f; } + .xterm-fg-120 { color: #87ff87; } + .xterm-fg-121 { color: #87ffaf; } + .xterm-fg-122 { color: #87ffd7; } + .xterm-fg-123 { color: #87ffff; } + .xterm-fg-124 { color: #af0000; } + .xterm-fg-125 { color: #af005f; } + .xterm-fg-126 { color: #af0087; } + .xterm-fg-127 { color: #af00af; } + .xterm-fg-128 { color: #af00d7; } + .xterm-fg-129 { color: #af00ff; } + .xterm-fg-130 { color: #af5f00; } + .xterm-fg-131 { color: #af5f5f; } + .xterm-fg-132 { color: #af5f87; } + .xterm-fg-133 { color: #af5faf; } + .xterm-fg-134 { color: #af5fd7; } + .xterm-fg-135 { color: #af5fff; } + .xterm-fg-136 { color: #af8700; } + .xterm-fg-137 { color: #af875f; } + .xterm-fg-138 { color: #af8787; } + .xterm-fg-139 { color: #af87af; } + .xterm-fg-140 { color: #af87d7; } + .xterm-fg-141 { color: #af87ff; } + .xterm-fg-142 { color: #afaf00; } + .xterm-fg-143 { color: #afaf5f; } + .xterm-fg-144 { color: #afaf87; } + .xterm-fg-145 { color: #afafaf; } + .xterm-fg-146 { color: #afafd7; } + .xterm-fg-147 { color: #afafff; } + .xterm-fg-148 { color: #afd700; } + .xterm-fg-149 { color: #afd75f; } + .xterm-fg-150 { color: #afd787; } + .xterm-fg-151 { color: #afd7af; } + .xterm-fg-152 { color: #afd7d7; } + .xterm-fg-153 { color: #afd7ff; } + .xterm-fg-154 { color: #afff00; } + .xterm-fg-155 { color: #afff5f; } + .xterm-fg-156 { color: #afff87; } + .xterm-fg-157 { color: #afffaf; } + .xterm-fg-158 { color: #afffd7; } + .xterm-fg-159 { color: #afffff; } + .xterm-fg-160 { color: #d70000; } + .xterm-fg-161 { color: #d7005f; } + .xterm-fg-162 { color: #d70087; } + .xterm-fg-163 { color: #d700af; } + .xterm-fg-164 { color: #d700d7; } + .xterm-fg-165 { color: #d700ff; } + .xterm-fg-166 { color: #d75f00; } + .xterm-fg-167 { color: #d75f5f; } + .xterm-fg-168 { color: #d75f87; } + .xterm-fg-169 { color: #d75faf; } + .xterm-fg-170 { color: #d75fd7; } + .xterm-fg-171 { color: #d75fff; } + .xterm-fg-172 { color: #d78700; } + .xterm-fg-173 { color: #d7875f; } + .xterm-fg-174 { color: #d78787; } + .xterm-fg-175 { color: #d787af; } + .xterm-fg-176 { color: #d787d7; } + .xterm-fg-177 { color: #d787ff; } + .xterm-fg-178 { color: #d7af00; } + .xterm-fg-179 { color: #d7af5f; } + .xterm-fg-180 { color: #d7af87; } + .xterm-fg-181 { color: #d7afaf; } + .xterm-fg-182 { color: #d7afd7; } + .xterm-fg-183 { color: #d7afff; } + .xterm-fg-184 { color: #d7d700; } + .xterm-fg-185 { color: #d7d75f; } + .xterm-fg-186 { color: #d7d787; } + .xterm-fg-187 { color: #d7d7af; } + .xterm-fg-188 { color: #d7d7d7; } + .xterm-fg-189 { color: #d7d7ff; } + .xterm-fg-190 { color: #d7ff00; } + .xterm-fg-191 { color: #d7ff5f; } + .xterm-fg-192 { color: #d7ff87; } + .xterm-fg-193 { color: #d7ffaf; } + .xterm-fg-194 { color: #d7ffd7; } + .xterm-fg-195 { color: #d7ffff; } + .xterm-fg-196 { color: #f00; } + .xterm-fg-197 { color: #ff005f; } + .xterm-fg-198 { color: #ff0087; } + .xterm-fg-199 { color: #ff00af; } + .xterm-fg-200 { color: #ff00d7; } + .xterm-fg-201 { color: #f0f; } + .xterm-fg-202 { color: #ff5f00; } + .xterm-fg-203 { color: #ff5f5f; } + .xterm-fg-204 { color: #ff5f87; } + .xterm-fg-205 { color: #ff5faf; } + .xterm-fg-206 { color: #ff5fd7; } + .xterm-fg-207 { color: #ff5fff; } + .xterm-fg-208 { color: #ff8700; } + .xterm-fg-209 { color: #ff875f; } + .xterm-fg-210 { color: #ff8787; } + .xterm-fg-211 { color: #ff87af; } + .xterm-fg-212 { color: #ff87d7; } + .xterm-fg-213 { color: #ff87ff; } + .xterm-fg-214 { color: #ffaf00; } + .xterm-fg-215 { color: #ffaf5f; } + .xterm-fg-216 { color: #ffaf87; } + .xterm-fg-217 { color: #ffafaf; } + .xterm-fg-218 { color: #ffafd7; } + .xterm-fg-219 { color: #ffafff; } + .xterm-fg-220 { color: #ffd700; } + .xterm-fg-221 { color: #ffd75f; } + .xterm-fg-222 { color: #ffd787; } + .xterm-fg-223 { color: #ffd7af; } + .xterm-fg-224 { color: #ffd7d7; } + .xterm-fg-225 { color: #ffd7ff; } + .xterm-fg-226 { color: #ff0; } + .xterm-fg-227 { color: #ffff5f; } + .xterm-fg-228 { color: #ffff87; } + .xterm-fg-229 { color: #ffffaf; } + .xterm-fg-230 { color: #ffffd7; } + .xterm-fg-231 { color: #fff; } + .xterm-fg-232 { color: #080808; } + .xterm-fg-233 { color: #121212; } + .xterm-fg-234 { color: #1c1c1c; } + .xterm-fg-235 { color: #262626; } + .xterm-fg-236 { color: #303030; } + .xterm-fg-237 { color: #3a3a3a; } + .xterm-fg-238 { color: #444; } + .xterm-fg-239 { color: #4e4e4e; } + .xterm-fg-240 { color: #585858; } + .xterm-fg-241 { color: #626262; } + .xterm-fg-242 { color: #6c6c6c; } + .xterm-fg-243 { color: #767676; } + .xterm-fg-244 { color: #808080; } + .xterm-fg-245 { color: #8a8a8a; } + .xterm-fg-246 { color: #949494; } + .xterm-fg-247 { color: #9e9e9e; } + .xterm-fg-248 { color: #a8a8a8; } + .xterm-fg-249 { color: #b2b2b2; } + .xterm-fg-250 { color: #bcbcbc; } + .xterm-fg-251 { color: #c6c6c6; } + .xterm-fg-252 { color: #d0d0d0; } + .xterm-fg-253 { color: #dadada; } + .xterm-fg-254 { color: #e4e4e4; } + .xterm-fg-255 { color: #eee; } diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 82055006ac0..762e36ee2e9 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController end def preview - @message = broadcast_message_params[:message] + @broadcast_message = BroadcastMessage.new(broadcast_message_params) end protected diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bd4ba384b29..705824502eb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -45,6 +45,10 @@ class ApplicationController < ActionController::Base redirect_to request.referer.present? ? :back : default, options end + def not_found + render_404 + end + protected # This filter handles both private tokens and personal access tokens @@ -173,7 +177,8 @@ class ApplicationController < ActionController::Base end def event_filter - filters = cookies['event_filter'].split(',') if cookies['event_filter'].present? + # Split using comma to maintain backward compatibility Ex/ "filter1,filter2" + filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present? @event_filter ||= EventFilter.new(filters) end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb deleted file mode 100644 index 5bb7d499cdc..00000000000 --- a/app/controllers/ci/application_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - class ApplicationController < ::ApplicationController - def self.railtie_helpers_paths - "app/helpers/ci" - end - end -end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 78012960252..3eb485de9db 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,5 +1,5 @@ module Ci - class LintsController < ApplicationController + class LintsController < ::ApplicationController before_action :authenticate_user! def show diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index aa894fde36b..ff297d6ff13 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,5 +1,5 @@ module Ci - class ProjectsController < Ci::ApplicationController + class ProjectsController < ::ApplicationController before_action :project before_action :no_cache, only: [:badge] before_action :authorize_read_project!, except: [:badge, :index] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 38e5943eb76..a62c6211372 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = TrendingProjectsFinder.new.execute - @projects = filter_projects(@projects) + @projects = filter_projects(Project.trending) @projects = @projects.page(params[:page]) respond_to do |format| diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb deleted file mode 100644 index 83eec1bf4a2..00000000000 --- a/app/controllers/namespaces_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -class NamespacesController < ApplicationController - skip_before_action :authenticate_user! - - def show - namespace = Namespace.find_by(path: params[:id]) - - if namespace - if namespace.is_a?(Group) - group = namespace - else - user = namespace.owner - end - end - - if user - redirect_to user_path(user) - elsif group && can?(current_user, :read_group, group) - redirect_to group_path(group) - elsif current_user.nil? - authenticate_user! - else - render_404 - end - end -end diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb deleted file mode 100644 index 3cfb08d5822..00000000000 --- a/app/controllers/projects/board_lists_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -class Projects::BoardListsController < Projects::ApplicationController - respond_to :json - - before_action :authorize_admin_list! - - rescue_from ActiveRecord::RecordNotFound, with: :record_not_found - - def create - list = Boards::Lists::CreateService.new(project, current_user, list_params).execute - - if list.valid? - render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } }) - else - render json: list.errors, status: :unprocessable_entity - end - end - - def update - service = Boards::Lists::MoveService.new(project, current_user, move_params) - - if service.execute - head :ok - else - head :unprocessable_entity - end - end - - def destroy - service = Boards::Lists::DestroyService.new(project, current_user, params) - - if service.execute - head :ok - else - head :unprocessable_entity - end - end - - def generate - service = Boards::Lists::GenerateService.new(project, current_user) - - if service.execute - render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } }) - else - head :unprocessable_entity - end - end - - private - - def authorize_admin_list! - return render_403 unless can?(current_user, :admin_list, project) - end - - def list_params - params.require(:list).permit(:label_id) - end - - def move_params - params.require(:list).permit(:position).merge(id: params[:id]) - end - - def record_not_found(exception) - render json: { error: exception.message }, status: :not_found - end -end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 4aa7982eab4..71eb56aed0b 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -2,6 +2,7 @@ module Projects module Boards class IssuesController < Boards::ApplicationController before_action :authorize_read_issue!, only: [:index] + before_action :authorize_create_issue!, only: [:create] before_action :authorize_update_issue!, only: [:update] def index @@ -9,16 +10,22 @@ module Projects issues = issues.page(params[:page]) render json: { - issues: issues.as_json( - only: [:iid, :title, :confidential], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } - }), + issues: serialize_as_json(issues), size: issues.total_count } end + def create + service = ::Boards::Issues::CreateService.new(project, current_user, issue_params) + issue = service.execute + + if issue.valid? + render json: serialize_as_json(issue) + else + render json: issue.errors, status: :unprocessable_entity + end + end + def update service = ::Boards::Issues::MoveService.new(project, current_user, move_params) @@ -43,16 +50,33 @@ module Projects return render_403 unless can?(current_user, :read_issue, project) end + def authorize_create_issue! + return render_403 unless can?(current_user, :admin_issue, project) + end + def authorize_update_issue! return render_403 unless can?(current_user, :update_issue, issue) end def filter_params - params.merge(id: params[:list_id]) + params.merge(board_id: params[:board_id], id: params[:list_id]) end def move_params - params.permit(:id, :from_list_id, :to_list_id) + params.permit(:board_id, :id, :from_list_id, :to_list_id) + end + + def issue_params + params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request) + end + + def serialize_as_json(resource) + resource.as_json( + only: [:iid, :title, :confidential], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + }) end end end diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index b995f586737..76ae41319c4 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -5,11 +5,11 @@ module Projects before_action :authorize_read_list!, only: [:index] def index - render json: serialize_as_json(project.board.lists) + render json: serialize_as_json(board.lists) end def create - list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute + list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board) if list.valid? render json: serialize_as_json(list) @@ -19,7 +19,7 @@ module Projects end def update - list = project.board.lists.movable.find(params[:id]) + list = board.lists.movable.find(params[:id]) service = ::Boards::Lists::MoveService.new(project, current_user, move_params) if service.execute(list) @@ -30,8 +30,8 @@ module Projects end def destroy - list = project.board.lists.destroyable.find(params[:id]) - service = ::Boards::Lists::DestroyService.new(project, current_user, params) + list = board.lists.destroyable.find(params[:id]) + service = ::Boards::Lists::DestroyService.new(project, current_user) if service.execute(list) head :ok @@ -43,8 +43,8 @@ module Projects def generate service = ::Boards::Lists::GenerateService.new(project, current_user) - if service.execute - render json: serialize_as_json(project.board.lists.movable) + if service.execute(board) + render json: serialize_as_json(board.lists.movable) else head :unprocessable_entity end @@ -60,6 +60,10 @@ module Projects return render_403 unless can?(current_user, :read_list, project) end + def board + @board ||= project.boards.find(params[:board_id]) + end + def list_params params.require(:list).permit(:label_id) end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 0035633b774..808affa4f98 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,12 +1,28 @@ class Projects::BoardsController < Projects::ApplicationController include IssuableCollections - - respond_to :html - before_action :authorize_read_board!, only: [:show] + before_action :authorize_read_board!, only: [:index, :show] + + def index + @boards = ::Boards::ListService.new(project, current_user).execute + + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(@boards) + end + end + end def show - ::Boards::CreateService.new(project, current_user).execute + @board = project.boards.find(params[:id]) + + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(@board) + end + end end private @@ -14,4 +30,8 @@ class Projects::BoardsController < Projects::ApplicationController def authorize_read_board! return access_denied! unless can?(current_user, :read_board, project) end + + def serialize_as_json(resource) + resource.as_json(only: [:id]) + end end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 092ef32e6e3..923e7340e69 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -38,12 +38,12 @@ class Projects::GraphsController < Projects::ApplicationController @languages = @languages.map do |language| name, share = language - color = Digest::SHA256.hexdigest(name)[0...6] + 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}" + color: color, + highlight: color } end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 7a7475a7345..ae060abee5c 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -1,6 +1,7 @@ class Projects::GroupLinksController < Projects::ApplicationController layout 'project_settings' before_action :authorize_admin_project! + before_action :authorize_admin_project_member!, only: [:update] def index @group_links = project.project_group_links.all @@ -27,9 +28,26 @@ class Projects::GroupLinksController < Projects::ApplicationController redirect_to namespace_project_group_links_path(project.namespace, project) end + def update + @group_link = @project.project_group_links.find(params[:id]) + + @group_link.update_attributes(group_link_params) + end + def destroy project.project_group_links.find(params[:id]).destroy - redirect_to namespace_project_group_links_path(project.namespace, project) + respond_to do |format| + format.html do + redirect_to namespace_project_group_links_path(project.namespace, project) + end + format.js { head :ok } + end + end + + protected + + def group_link_params + params.require(:group_link).permit(:group_access, :expires_at) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ef13e0677d2..96041b07647 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController protected def issue - @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old + # The Sortable default scope causes performance issues when used with find_by + @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old end alias_method :subscribable_resource, :issue alias_method :issuable, :issue diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 8c8c56228ad..9207c954335 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, - :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts + :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] @@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :apply_diff_view_cookie!, only: [:new_diffs] + before_action :build_merge_request, only: [:new, :new_diffs] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -29,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Allow modify merge_request before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :authenticate_user!, only: [:assign_related_issues] + before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] def index @@ -210,29 +214,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def new - apply_diff_view_cookie! - - build_merge_request - @noteable = @merge_request - - @target_branches = if @merge_request.target_project - @merge_request.target_project.repository.branch_names - else - [] - end - - @target_project = merge_request.target_project - @source_project = merge_request.source_project - @commits = @merge_request.compare_commits.reverse - @commit = @merge_request.diff_head_commit - @base_commit = @merge_request.diff_base_commit - @diffs = @merge_request.diffs(diff_options) if @merge_request.compare - @diff_notes_disabled = true - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline + define_new_vars + end - @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + def new_diffs + respond_to do |format| + format.html do + define_new_vars + render "new" + end + format.json do + @diffs = if @merge_request.can_be_created + @merge_request.diffs(diff_options) + else + [] + end + @diff_notes_disabled = true + + render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) } + end + end end def create @@ -355,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController render layout: false end + def assign_related_issues + result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute + + respond_to do |format| + format.html do + case result[:count] + when 0 + flash[:error] = "Failed to assign you issues related to the merge request" + when 1 + flash[:notice] = "1 issue has been assigned to you" + else + flash[:notice] = "#{result[:count]} issues have been assigned to you" + end + + redirect_to(merge_request_path(@merge_request)) + end + end + end + def ci_status pipeline = @merge_request.pipeline if pipeline @@ -383,6 +403,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def ci_environments_status + environments = + begin + @merge_request.environments.map do |environment| + next unless can?(current_user, :read_environment, environment) + + project = environment.project + deployment = environment.first_deployment_for(@merge_request.diff_head_commit) + + { + id: environment.id, + name: environment.name, + url: namespace_project_environment_path(project.namespace, project, environment), + external_url: environment.external_url, + external_url_formatted: environment.formatted_external_url, + deployed_at: deployment.try(:created_at), + deployed_at_formatted: deployment.try(:formatted_deployment_time) + } + end.compact + end + + render json: environments + end + protected def selected_target_project @@ -490,6 +534,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def define_new_vars + @noteable = @merge_request + + @target_branches = if @merge_request.target_project + @merge_request.target_project.repository.branch_names + else + [] + end + + @target_project = merge_request.target_project + @source_project = merge_request.source_project + @commits = @merge_request.compare_commits.reverse + @commit = @merge_request.diff_head_commit + @base_commit = @merge_request.diff_base_commit + + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses.relevant if @pipeline + @note_counts = Note.where(commit_id: @commits.map(&:id)). + group(:commit_id).count + end + def invalid_mr # Render special view for MR with removed target branch render 'invalid' @@ -521,7 +586,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def build_merge_request params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) - @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute + @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute end def compared_diff_version diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index f56b256984b..37a86ed0523 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -5,34 +5,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index + @group_links = @project.project_group_links + @project_members = @project.project_members @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) if params[:search].present? users = @project.users.search(params[:search]).to_a @project_members = @project_members.where(user_id: users) - end - - @project_members = @project_members.order('access_level DESC') - - @group = @project.group - - if @group - @group_members = @group.group_members - @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) - - if params[:search].present? - users = @group.users.search(params[:search]).to_a - @group_members = @group_members.where(user_id: users) - end - @group_members = @group_members.order('access_level DESC') + @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end + @project_members = @project_members.order(access_level: :desc).page(params[:page]) + @requesters = AccessRequestsFinder.new(@project).execute(current_user) @project_member = @project.project_members.new - @project_group_links = @project.project_group_links end def create @@ -43,6 +32,21 @@ class Projects::ProjectMembersController < Projects::ApplicationController current_user: current_user ) + if params[:group_ids].present? + group_ids = params[:group_ids].split(',') + groups = Group.where(id: group_ids) + + groups.each do |group| + next unless can?(current_user, :read_group, group) + + project.project_group_links.create( + group: group, + group_access: params[:access_level], + expires_at: params[:expires_at] + ) + end + end + redirect_to namespace_project_project_members_path(@project.namespace, @project) end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 6ea8ee62bc5..8fea20cefef 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController def show @tag = @repository.find_tag(params[:id]) + return render_404 unless @tag + @release = @project.releases.find_or_initialize_by(tag: @tag.name) @commit = @repository.commit(@tag.target) end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index d198782138a..dee57e4a388 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,10 +1,10 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show, :raw] + before_action :authorize_read_snippet!, only: [:show, :raw, :download] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -12,7 +12,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] layout 'snippets' respond_to :html @@ -75,6 +75,14 @@ class SnippetsController < ApplicationController ) end + def download + send_data( + @snippet.content, + type: 'text/plain; charset=utf-8', + filename: @snippet.sanitized_file_name + ) + end + protected def snippet diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 838ecc837e4..6a881b271d7 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController skip_before_action :authenticate_user! - before_action :user + before_action :user, except: [:exists] before_action :authorize_read_user!, only: [:show] def show @@ -85,6 +85,10 @@ class UsersController < ApplicationController render 'calendar_activities', layout: false end + def exists + render json: { exists: Namespace.where(path: params[:username].downcase).any? } + end + private def authorize_read_user! diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb deleted file mode 100644 index c1e434d9926..00000000000 --- a/app/finders/trending_projects_finder.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Finder for retrieving public trending projects in a given time range. -class TrendingProjectsFinder - # current_user - The currently logged in User, if any. - # last_months - The number of months to limit the trending data to. - def execute(months_limit = 1) - Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do - Project.public_only.trending(months_limit.months.ago) - end - end - - private - - def cache_key_for(months) - "trending_projects/#{months}" - end -end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index de13e7a1fc2..16136d02530 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -16,7 +16,7 @@ module AppearancesHelper end def brand_text - markdown(brand_item.description) + markdown_field(brand_item, :description) end def brand_item diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 6de25bea654..6229384817b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -11,18 +11,6 @@ module ApplicationSettingsHelper current_application_settings.signin_enabled? end - def extra_sign_in_text - current_application_settings.sign_in_text - end - - def after_sign_up_text - current_application_settings.after_sign_up_text - end - - def shared_runners_text - current_application_settings.shared_runners_text - end - def user_oauth_applications? current_application_settings.user_oauth_applications end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index df41473543b..b7e0ff8ecd0 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -4,15 +4,18 @@ module AvatarsHelper user: commit_or_event.author, user_name: commit_or_event.author_name, user_email: commit_or_event.author_email, + css_class: 'hidden-xs' })) end def user_avatar(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] + css_class = options[:css_class] || '' + avatar = image_tag( avatar_icon(options[:user] || options[:user_email], avatar_size), - class: "avatar has-tooltip hidden-xs s#{avatar_size}", + class: "avatar has-tooltip s#{avatar_size} #{css_class}", alt: "#{user_name}'s avatar", title: user_name, data: { container: 'body' } diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb new file mode 100644 index 00000000000..b7247ffa8b2 --- /dev/null +++ b/app/helpers/boards_helper.rb @@ -0,0 +1,12 @@ +module BoardsHelper + def board_data + board = @board || @boards.first + + { + endpoint: namespace_project_boards_path(@project.namespace, @project), + board_id: board.id, + disabled: !can?(current_user, :admin_list, @project), + issue_link_base: namespace_project_issues_path(@project.namespace, @project) + } + end +end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 43a29c96bca..eb03ced67eb 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -3,7 +3,7 @@ module BroadcastMessagesHelper return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do - icon('bullhorn') << ' ' << render_broadcast_message(message.message) + icon('bullhorn') << ' ' << render_broadcast_message(message) end end @@ -32,7 +32,7 @@ module BroadcastMessagesHelper end end - def render_broadcast_message(message) - Banzai.render(message, pipeline: :broadcast_message).html_safe + def render_broadcast_message(broadcast_message) + Banzai.render_field(broadcast_message, :message).html_safe end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index b478580978b..85e1dc33ee8 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -15,13 +15,14 @@ module ButtonHelper # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) + css_class = data[:class] || 'btn-clipboard btn-transparent' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), - class: "btn btn-clipboard", + class: "btn #{css_class}", data: data, type: :button, - title: "Copy to Clipboard" + title: 'Copy to Clipboard' end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1a259656f31..0772d848289 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -13,14 +13,12 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body.start_with?('<img') - body - else - escape_once(body) - end - - user = current_user if defined?(current_user) - gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) + context = { + project: @project, + current_user: (current_user if defined?(current_user)), + pipeline: :single_line, + } + gfm_body = Banzai.render(body, context) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -51,17 +49,15 @@ module GitlabMarkdownHelper context[:project] ||= @project html = Banzai.render(text, context) + banzai_postprocess(html, context) + end - context.merge!( - current_user: (current_user if defined?(current_user)), - - # RelativeLinkFilter - requested_path: @path, - project_wiki: @project_wiki, - ref: @ref - ) + def markdown_field(object, field) + object = object.for_display if object.respond_to?(:for_display) + return "" unless object.present? - Banzai.post_process(html, context) + html = Banzai.render_field(object, field) + banzai_postprocess(html, object.banzai_render_context(field)) end def asciidoc(text) @@ -196,4 +192,18 @@ module GitlabMarkdownHelper icon(options[:icon]) end end + + # Calls Banzai.post_process with some common context options + def banzai_postprocess(html, context) + context.merge!( + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + requested_path: @path, + project_wiki: @project_wiki, + ref: @ref + ) + + Banzai.post_process(html, context) + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 8b212b0327a..1644c346dd8 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -113,14 +113,13 @@ module IssuesHelper end end - def award_user_list(awards, current_user) + def award_user_list(awards, current_user, limit: 10) names = awards.map do |award| award.user == current_user ? 'You' : award.user.name end - # Take first 9 OR current user + first 9 current_user_name = names.delete('You') - names = names.first(9).insert(0, current_user_name).compact + names = names.insert(0, current_user_name).compact.first(limit) names << "#{awards.size - names.size} more." if awards.size > names.size diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8abe7865fed..249cb44e9d5 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -72,6 +72,19 @@ module MergeRequestsHelper ) end + def mr_assign_issues_link + issues = MergeRequests::AssignIssuesService.new(@project, + current_user, + merge_request: @merge_request, + closes_issues: mr_closes_issues + ).assignable_issues + path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + if issues.present? + pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue" + link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post + end + end + def source_branch_with_namespace(merge_request) branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) @@ -110,4 +123,8 @@ module MergeRequestsHelper def version_index(merge_request_diff) @merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff) end + + def different_base?(version1, version2) + version1 && version2 && version1.base_commit_sha != version2.base_commit_sha + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8a7446b7cc7..aba3a3f9c5d 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -153,8 +153,18 @@ module SearchHelper search_path(options) end - # Sanitize html generated after parsing markdown from issue description or comment - def search_md_sanitize(html) + # Sanitize a HTML field for search display. Most tags are stripped out and the + # maximum length is set to 200 characters. + def search_md_sanitize(object, field) + html = markdown_field(object, field) + html = Truncato.truncate( + html, + count_tags: false, + count_tail: false, + max_length: 200 + ) + + # Truncato's filtered_tags and filtered_attributes are not quite the same sanitize(html, tags: %w(a p ol ul li pre code)) end end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index b01a244032d..2340453831e 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -1,4 +1,8 @@ class AbuseReport < ActiveRecord::Base + include CacheMarkdownField + + cache_markdown_field :message, pipeline: :single_line + belongs_to :reporter, class_name: 'User' belongs_to :user @@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base validates :message, presence: true validates :user_id, uniqueness: { message: 'has already been reported' } + # For CacheMarkdownField + alias_method :author, :reporter + def remove_user(deleted_by:) user.block DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 4cf8dd9a8ce..e4106e1c2e9 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -1,4 +1,8 @@ class Appearance < ActiveRecord::Base + include CacheMarkdownField + + cache_markdown_field :description + validates :title, presence: true validates :description, presence: true validates :logo, file_size: { maximum: 1.megabyte } diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 55d2e07de08..c99aa7772bb 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,5 +1,7 @@ class ApplicationSetting < ActiveRecord::Base + include CacheMarkdownField include TokenAuthenticatable + add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token @@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base serialize :domain_whitelist, Array serialize :domain_blacklist, Array + cache_markdown_field :sign_in_text + cache_markdown_field :help_page_text + cache_markdown_field :shared_runners_text, pipeline: :plain_markdown + cache_markdown_field :after_sign_up_text + attr_accessor :domain_whitelist_raw, :domain_blacklist_raw validates :session_expire_delay, diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 61498140f27..cb40f33932a 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -1,6 +1,9 @@ class BroadcastMessage < ActiveRecord::Base + include CacheMarkdownField include Sortable + cache_markdown_field :message, pipeline: :broadcast_message + validates :message, presence: true validates :starts_at, presence: true validates :ends_at, presence: true diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 09c5223c60d..fd762b8c5ce 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1,6 +1,7 @@ module Ci class Build < CommitStatus include TokenAuthenticatable + include AfterCommitQueue belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' @@ -75,25 +76,20 @@ module Ci state_machine :status do after_transition pending: :running do |build| - build.execute_hooks + build.run_after_commit do + BuildHooksWorker.perform_async(id) + end end after_transition any => [:success, :failed, :canceled] do |build| - build.update_coverage - build.execute_hooks + build.run_after_commit do + BuildFinishedWorker.perform_async(id) + end end after_transition any => [:success] do |build| - if build.environment.present? - service = CreateDeploymentService.new( - build.project, build.user, - environment: build.environment, - sha: build.sha, - ref: build.ref, - tag: build.tag, - options: build.options.to_h[:environment], - variables: build.variables) - service.execute(build) + build.run_after_commit do + BuildSuccessWorker.perform_async(id) end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2cf9892edc5..4fdb5fef4fb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -3,6 +3,7 @@ module Ci extend Ci::Model include HasStatus include Importable + include AfterCommitQueue self.table_name = 'ci_commits' @@ -56,6 +57,10 @@ module Ci pipeline.finished_at = Time.now end + before_transition do |pipeline| + pipeline.update_duration + end + after_transition [:created, :pending] => :running do |pipeline| MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) @@ -66,12 +71,16 @@ module Ci update_all(latest_build_finished_at: pipeline.finished_at) end - before_transition do |pipeline| - pipeline.update_duration + after_transition [:created, :pending, :running] => :success do |pipeline| + pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) } end after_transition do |pipeline, transition| - pipeline.execute_hooks unless transition.loopback? + next if transition.loopback? + + pipeline.run_after_commit do + PipelineHooksWorker.perform_async(id) + end end end @@ -292,11 +301,9 @@ module Ci # Merge requests for which the current pipeline is running against # the merge request's latest commit. def merge_requests - @merge_requests ||= - begin - project.merge_requests.where(source_branch: self.ref). - select { |merge_request| merge_request.pipeline.try(:id) == self.id } - end + @merge_requests ||= project.merge_requests + .where(source_branch: self.ref) + .select { |merge_request| merge_request.pipeline.try(:id) == self.id } end private diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 9fa8d17e74e..7b554be4f9a 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,6 +1,7 @@ class CommitStatus < ActiveRecord::Base include HasStatus include Importable + include AfterCommitQueue self.table_name = 'ci_builds' @@ -85,25 +86,24 @@ class CommitStatus < ActiveRecord::Base end after_transition do |commit_status, transition| - commit_status.pipeline.try do |pipeline| - break if transition.loopback? - - if commit_status.complete? - ProcessPipelineWorker.perform_async(pipeline.id) + next if transition.loopback? + + commit_status.run_after_commit do + pipeline.try do |pipeline| + if complete? + PipelineProcessWorker.perform_async(pipeline.id) + else + PipelineUpdateWorker.perform_async(pipeline.id) + end end - - UpdatePipelineWorker.perform_async(pipeline.id) end - - true - end - - after_transition [:created, :pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) end after_transition any => :failed do |commit_status| - MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) + commit_status.run_after_commit do + MergeRequests::AddTodoWhenBuildFailsService + .new(pipeline.project, nil).execute(self) + end end end diff --git a/app/models/compare.rb b/app/models/compare.rb index 4856510f526..3a8bbcb1acd 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -11,9 +11,10 @@ class Compare end end - def initialize(compare, project) + def initialize(compare, project, straight: false) @compare = compare @project = project + @straight = straight end def commits @@ -45,6 +46,18 @@ class Compare end end + def start_commit_sha + start_commit.try(:sha) + end + + def base_commit_sha + base_commit.try(:sha) + end + + def head_commit_sha + commit.try(:sha) + end + def raw_diffs(*args) @compare.diffs(*args) end @@ -58,9 +71,9 @@ class Compare def diff_refs Gitlab::Diff::DiffRefs.new( - base_sha: base_commit.try(:sha), - start_sha: start_commit.try(:sha), - head_sha: commit.try(:sha) + base_sha: @straight ? start_commit_sha : base_commit_sha, + start_sha: start_commit_sha, + head_sha: head_commit_sha ) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb new file mode 100644 index 00000000000..90bd6490a02 --- /dev/null +++ b/app/models/concerns/cache_markdown_field.rb @@ -0,0 +1,131 @@ +# This module takes care of updating cache columns for Markdown-containing +# fields. Use like this in the body of your class: +# +# include CacheMarkdownField +# cache_markdown_field :foo +# cache_markdown_field :bar +# cache_markdown_field :baz, pipeline: :single_line +# +# Corresponding foo_html, bar_html and baz_html fields should exist. +module CacheMarkdownField + # Knows about the relationship between markdown and html field names, and + # stores the rendering contexts for the latter + class FieldData + extend Forwardable + + def initialize + @data = {} + end + + def_delegators :@data, :[], :[]= + def_delegator :@data, :keys, :markdown_fields + + def html_field(markdown_field) + "#{markdown_field}_html" + end + + def html_fields + markdown_fields.map {|field| html_field(field) } + end + end + + # Dynamic registries don't really work in Rails as it's not guaranteed that + # every class will be loaded, so hardcode the list. + CACHING_CLASSES = %w[ + AbuseReport + Appearance + ApplicationSetting + BroadcastMessage + Issue + Label + MergeRequest + Milestone + Namespace + Note + Project + Release + Snippet + ] + + def self.caching_classes + CACHING_CLASSES.map(&:constantize) + end + + extend ActiveSupport::Concern + + included do + cattr_reader :cached_markdown_fields do + FieldData.new + end + + # Returns the default Banzai render context for the cached markdown field. + def banzai_render_context(field) + raise ArgumentError.new("Unknown field: #{field.inspect}") unless + cached_markdown_fields.markdown_fields.include?(field) + + # Always include a project key, or Banzai complains + project = self.project if self.respond_to?(:project) + context = cached_markdown_fields[field].merge(project: project) + + # Banzai is less strict about authors, so don't always have an author key + context[:author] = self.author if self.respond_to?(:author) + + context + end + + # Allow callers to look up the cache field name, rather than hardcoding it + def markdown_cache_field_for(field) + raise ArgumentError.new("Unknown field: #{field}") unless + cached_markdown_fields.markdown_fields.include?(field) + + cached_markdown_fields.html_field(field) + end + + # Always exclude _html fields from attributes (including serialization). + # They contain unredacted HTML, which would be a security issue + alias_method :attributes_before_markdown_cache, :attributes + def attributes + attrs = attributes_before_markdown_cache + + cached_markdown_fields.html_fields.each do |field| + attrs.delete(field) + end + + attrs + end + end + + class_methods do + private + + # Specify that a field is markdown. Its rendered output will be cached in + # a corresponding _html field. Any custom rendering options may be provided + # as a context. + def cache_markdown_field(markdown_field, context = {}) + raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless + CacheMarkdownField::CACHING_CLASSES.include?(self.to_s) + + cached_markdown_fields[markdown_field] = context + + html_field = cached_markdown_fields.html_field(markdown_field) + cache_method = "#{markdown_field}_cache_refresh".to_sym + invalidation_method = "#{html_field}_invalidated?".to_sym + + define_method(cache_method) do + html = Banzai::Renderer.cacheless_render_field(self, markdown_field) + __send__("#{html_field}=", html) + true + end + + # The HTML becomes invalid if any dependent fields change. For now, assume + # author and project invalidate the cache in all circumstances. + define_method(invalidation_method) do + changed_fields = changed_attributes.keys + invalidations = changed_fields & [markdown_field.to_s, "author", "project"] + !invalidations.empty? + end + + before_save cache_method, if: invalidation_method + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index ff465d2c745..c4b42ad82c7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,6 +6,7 @@ # module Issuable extend ActiveSupport::Concern + include CacheMarkdownField include Participable include Mentionable include Subscribable @@ -13,6 +14,9 @@ module Issuable include Awardable included do + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description + belongs_to :author, class_name: "User" belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index be295487fd2..8ed4a56b19b 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -2,6 +2,8 @@ class CycleAnalytics include Gitlab::Database::Median include Gitlab::Database::DateTime + DEPLOYMENT_METRIC_STAGES = %i[production staging] + def initialize(project, from:) @project = project @from = from @@ -66,7 +68,7 @@ class CycleAnalytics # cycle analytics stage. interval_query = Arel::Nodes::As.new( cte_table, - subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s)) + subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s)) median_datetime(cte_table, interval_query, name) end @@ -75,7 +77,7 @@ class CycleAnalytics # closes the given issue) with issue and merge request metrics included. The metrics # are loaded with an inner join, so issues / merge requests without metrics are # automatically excluded. - def base_query + def base_query_for(name) arel_table = MergeRequestsClosingIssues.arel_table # Load issues @@ -91,7 +93,11 @@ class CycleAnalytics join(MergeRequest::Metrics.arel_table). on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id])) - # Limit to merge requests that have been deployed to production after `@from` - query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from)) + if DEPLOYMENT_METRIC_STAGES.include?(name) + # Limit to merge requests that have been deployed to production after `@from` + query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from)) + end + + query end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 070c76339b1..054d54f124e 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -40,7 +40,14 @@ class Deployment < ActiveRecord::Base def includes_commit?(commit) return false unless commit - project.repository.is_ancestor?(commit.id, sha) + # Before 8.10, deployments didn't have keep-around refs. Any deployment + # created before then could have a `sha` referring to a commit that no + # longer exists in the repository, so just ignore those. + begin + project.repository.is_ancestor?(commit.id, sha) + rescue Rugged::OdbError + false + end end def update_merge_request_metrics! @@ -90,6 +97,10 @@ class Deployment < ActiveRecord::Base close_action.present? end + def formatted_deployment_time + created_at.to_time.in_time_zone.to_s(:medium) + end + private def ref_path diff --git a/app/models/environment.rb b/app/models/environment.rb index 6ec498ea2b7..07f14a7ad8d 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -66,7 +66,22 @@ class Environment < ActiveRecord::Base self.name == "production" end + def first_deployment_for(commit) + ref = project.repository.ref_name_for_sha(ref_path, commit.sha) + + return nil unless ref + + deployment_id = ref.split('/').last + deployments.find(deployment_id) + end + def ref_path "refs/environments/#{Shellwords.shellescape(name)}" end + + def formatted_external_url + return nil unless external_url + + external_url.gsub(/\A.*?:\/\//, '') + end end diff --git a/app/models/event.rb b/app/models/event.rb index 633019fe0af..0764cb8cabd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -68,8 +68,10 @@ class Event < ActiveRecord::Base true elsif issue? || issue_note? Ability.allowed?(user, :read_issue, note? ? note_target : target) + elsif merge_request? || merge_request_note? + Ability.allowed?(user, :read_merge_request, note? ? note_target : target) else - ((merge_request? || note?) && target.present?) || milestone? + milestone? end end @@ -280,6 +282,10 @@ class Event < ActiveRecord::Base note? && target && target.for_issue? end + def merge_request_note? + note? && target && target.for_merge_request? + end + def project_snippet_note? target.for_snippet? end @@ -335,7 +341,7 @@ class Event < ActiveRecord::Base # update the project. Only one query should actually perform the update, # hence we add the extra WHERE clause for last_activity_at. Project.unscoped.where(id: project_id). - where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). + where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). update_all(last_activity_at: created_at) end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index ddd4bad5c21..698a7bbd327 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -4,6 +4,10 @@ class GlobalLabel delegate :color, :description, to: :@first_label + def for_display + @first_label + end + def self.build_collection(labels) labels = labels.group_by(&:title) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index bda2b5c5d5d..cde4a568577 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -4,6 +4,10 @@ class GlobalMilestone attr_accessor :title, :milestones alias_attribute :name, :title + def for_display + @first_milestone + end + def self.build_collection(milestones) milestones = milestones.group_by(&:title) @@ -17,6 +21,7 @@ class GlobalMilestone @title = title @name = title @milestones = milestones + @first_milestone = milestones.find {|m| m.description.present? } || milestones.first end def safe_title diff --git a/app/models/label.rb b/app/models/label.rb index a23140b7d64..e8e12e2904e 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -1,4 +1,5 @@ class Label < ActiveRecord::Base + include CacheMarkdownField include Referable include Subscribable @@ -8,6 +9,8 @@ class Label < ActiveRecord::Base None = LabelStruct.new('No Label', 'No Label') Any = LabelStruct.new('Any Label', '') + cache_markdown_field :description, pipeline: :single_line + DEFAULT_COLOR = '#428BCA' default_value_for :color, DEFAULT_COLOR diff --git a/app/models/member.rb b/app/models/member.rb index 38a278ea559..b89ba8ecbb8 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -103,7 +103,12 @@ class Member < ActiveRecord::Base } if member.request? - ::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute + ::Members::ApproveAccessRequestService.new( + source, + current_user, + id: member.id, + access_level: access_level + ).execute else member.save end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 071dfe54ef9..5ccfe11a2a2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base # Temporary fields to store compare vars # when creating new merge request - attr_accessor :can_be_created, :compare_commits, :compare + attr_accessor :can_be_created, :compare_commits, :diff_options, :compare state_machine :state, initial: :opened do event :close do @@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base end def diff_size - merge_request_diff.size + diffs(diff_options).size end def diff_base_commit @@ -688,12 +688,15 @@ class MergeRequest < ActiveRecord::Base def environments return [] unless diff_head_commit - environments = source_project.environments_for( - source_branch, diff_head_commit) - environments += target_project.environments_for( - target_branch, diff_head_commit, with_tags: true) + @environments ||= + begin + environments = source_project.environments_for( + source_branch, diff_head_commit) + environments += target_project.environments_for( + target_branch, diff_head_commit, with_tags: true) - environments.uniq + environments.uniq + end end def state_human_name diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 36b8b70870b..b8a10b7968e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 + # Valid types of serialized diffs allowed by Gitlab::Git::Diff + VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta] + belongs_to :merge_request state_machine :state, initial: :empty do @@ -164,12 +167,24 @@ class MergeRequestDiff < ActiveRecord::Base self == merge_request.merge_request_diff end - def compare_with(sha) - CompareService.new.execute(project, head_commit_sha, project, sha) + def compare_with(sha, straight: true) + # When compare merge request versions we want diff A..B instead of A...B + # so we handle cases when user does squash and rebase of the commits between versions. + # For this reason we set straight to true by default. + CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) end private + # Old GitLab implementations may have generated diffs as ["--broken-diff"]. + # Avoid an error 500 by ignoring bad elements. See: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776 + def valid_raw_diff?(raw) + return false unless raw.respond_to?(:each) + + raw.any? { |element| VALID_CLASSES.include?(element.class) } + end + def dump_commits(commits) commits.map(&:to_hash) end @@ -200,7 +215,7 @@ class MergeRequestDiff < ActiveRecord::Base end def load_diffs(raw, options) - if raw.respond_to?(:each) + if valid_raw_diff?(raw) if paths = options[:paths] raw = raw.select do |diff| paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 44c3cbb2c73..23aecbfa3a6 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base Any = MilestoneStruct.new('Any Milestone', '', -1) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) + include CacheMarkdownField include InternalId include Sortable include Referable include StripAttribute include Milestoneish + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description + belongs_to :project has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 919b3b1f095..b67049f0f55 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,9 +1,12 @@ class Namespace < ActiveRecord::Base acts_as_paranoid + include CacheMarkdownField include Sortable include Gitlab::ShellAdapter + cache_markdown_field :description, pipeline: :description + has_many :projects, dependent: :destroy belongs_to :owner, class_name: "User" @@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base def clean_path(path) path = path.dup # Get the email username by removing everything after an `@` sign. - path.gsub!(/@.*\z/, "") - # Usernames can't end in .git, so remove it. - path.gsub!(/\.git\z/, "") - # Remove dashes at the start of the username. - path.gsub!(/\A-+/, "") - # Remove periods at the end of the username. - path.gsub!(/\.+\z/, "") + path.gsub!(/@.*\z/, "") # Remove everything that's not in the list of allowed characters. - path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + # Remove trailing violations ('.atom', '.git', or '.') + path.gsub!(/(\.atom|\.git|\.)*\z/, "") + # Remove leading violations ('-') + path.gsub!(/\A\-+/, "") # Users with the great usernames of "." or ".." would end up with a blank username. # Work around that by setting their username to "blank", followed by a counter. diff --git a/app/models/note.rb b/app/models/note.rb index f2656df028b..2d644b03e4d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -6,10 +6,13 @@ class Note < ActiveRecord::Base include Awardable include Importable include FasterCacheKeys + include CacheMarkdownField + + cache_markdown_field :note, pipeline: :note # Attribute containing rendered and redacted Markdown as generated by # Banzai::ObjectRenderer. - attr_accessor :note_html + attr_accessor :redacted_note_html # An Array containing the number of visible references as generated by # Banzai::ObjectRenderer diff --git a/app/models/project.rb b/app/models/project.rb index 12705f9ae48..2e8073733d4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -6,6 +6,7 @@ class Project < ActiveRecord::Base include Gitlab::VisibilityLevel include Gitlab::CurrentSettings include AccessRequestable + include CacheMarkdownField include Referable include Sortable include AfterCommitQueue @@ -15,8 +16,13 @@ class Project < ActiveRecord::Base extend Gitlab::ConfigHelper + class BoardLimitExceeded < StandardError; end + + NUMBER_OF_PERMITTED_BOARDS = 1 UNKNOWN_IMPORT_URL = 'http://unknown.git' + cache_markdown_field :description, pipeline: :description + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true default_value_for :archived, false @@ -62,8 +68,7 @@ class Project < ActiveRecord::Base belongs_to :namespace has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' - - has_one :board, dependent: :destroy + has_many :boards, before_add: :validate_board_limit, dependent: :destroy # Project services has_many :services @@ -372,19 +377,9 @@ class Project < ActiveRecord::Base %r{(?<project>#{name_pattern}/#{name_pattern})} end - def trending(since = 1.month.ago) - # By counting in the JOIN we don't expose the GROUP BY to the outer query. - # This means that calls such as "any?" and "count" just return a number of - # the total count, instead of the counts grouped per project as a Hash. - join_body = "INNER JOIN ( - SELECT project_id, COUNT(*) AS amount - FROM notes - WHERE created_at >= #{sanitize(since)} - AND system IS FALSE - GROUP BY project_id - ) join_note_counts ON projects.id = join_note_counts.project_id" - - joins(join_body).reorder('join_note_counts.amount DESC') + def trending + joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id'). + reorder('trending_projects.id ASC') end def cached_count @@ -495,7 +490,7 @@ class Project < ActiveRecord::Base end def import_url - if import_data && super + if import_data && super.present? import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials) import_url.full_url else @@ -834,11 +829,6 @@ class Project < ActiveRecord::Base end end - def update_merge_requests(oldrev, newrev, ref, user) - MergeRequests::RefreshService.new(self, user). - execute(oldrev, newrev, ref) - end - def valid_repo? repository.exists? rescue @@ -1346,4 +1336,8 @@ class Project < ActiveRecord::Base shared_projects.any? end + + def validate_board_limit(board) + raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS + end end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 7613cbdea93..db46def11eb 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base belongs_to :group validates :project_id, presence: true - validates :group_id, presence: true + validates :group, presence: true validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" } validates :group_access, presence: true validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true diff --git a/app/models/release.rb b/app/models/release.rb index e196b84eb18..c936899799e 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,4 +1,8 @@ class Release < ActiveRecord::Base + include CacheMarkdownField + + cache_markdown_field :description + belongs_to :project validates :description, :project, :tag, presence: true diff --git a/app/models/repository.rb b/app/models/repository.rb index eb574555df6..72e473871fa 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -111,8 +111,10 @@ class Repository def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) ref ||= root_ref - # Limited to 1000 commits for now, could be parameterized? - args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query}) + args = %W( + #{Gitlab.config.git.bin_path} 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.map(&:chomp) @@ -717,6 +719,14 @@ class Repository end end + def ref_name_for_sha(ref_path, sha) + args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) + + # Not found -> ["", 0] + # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] + Gitlab::Popen.popen(args, path_to_repo).first.split.last + 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 @@ -838,6 +848,52 @@ class Repository end end + def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil) + update_branch_with_hooks(user, branch) do |ref| + index = rugged.index + parents = [] + branch = find_branch(ref) + + if branch + last_commit = branch.target + index.read_tree(last_commit.raw_commit.tree) + parents = [last_commit.sha] + end + + actions.each do |action| + case action[:action] + when :create, :update, :move + mode = + case action[:action] + when :update + index.get(action[:file_path])[:mode] + when :move + index.get(action[:previous_path])[:mode] + end + mode ||= 0o100644 + + index.remove(action[:previous_path]) if action[:action] == :move + + content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content] + oid = rugged.write(content, :blob) + + index.add(path: action[:file_path], oid: oid, mode: mode) + when :delete + index.remove(action[:file_path]) + end + end + + options = { + tree: index.write_tree(rugged), + message: message, + parents: parents + } + options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) + + Rugged::Commit.create(rugged, options) + end + end + def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) author = Gitlab::Git::committer_hash(email: email, name: name) || committer @@ -968,7 +1024,8 @@ class Repository root_ref_commit = commit(root_ref) if branch_commit - is_ancestor?(branch_commit.id, root_ref_commit.id) + same_head = branch_commit.id == root_ref_commit.id + !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id) else nil end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8a1730f3f36..2373b445009 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -1,11 +1,21 @@ class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel include Linguist::BlobHelper + include CacheMarkdownField include Participable include Referable include Sortable include Awardable + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :content + + # If file_name changes, it invalidates content + alias_method :default_content_html_invalidator, :content_html_invalidated? + def content_html_invalidated? + default_content_html_invalidator || file_name_changed? + end + default_value_for :visibility_level, Snippet::PRIVATE belongs_to :author, class_name: 'User' diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb new file mode 100644 index 00000000000..27e3732da17 --- /dev/null +++ b/app/models/trending_project.rb @@ -0,0 +1,35 @@ +class TrendingProject < ActiveRecord::Base + belongs_to :project + + # The number of months to include in the trending calculation. + MONTHS_TO_INCLUDE = 1 + + # The maximum number of projects to include in the trending set. + PROJECTS_LIMIT = 100 + + # Populates the trending projects table with the current list of trending + # projects. + def self.refresh! + # The calculation **must** run in a transaction. If the removal of data and + # insertion of new data were to run separately a user might end up with an + # empty list of trending projects for a short period of time. + transaction do + delete_all + + timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago) + + connection.execute <<-EOF.strip_heredoc + INSERT INTO #{table_name} (project_id) + SELECT project_id + FROM notes + INNER JOIN projects ON projects.id = notes.project_id + WHERE notes.created_at >= #{timestamp} + AND notes.system IS FALSE + AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} + GROUP BY project_id + ORDER BY count(*) DESC + LIMIT #{PROJECTS_LIMIT}; + EOF + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 508efd85050..f367f4616fb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -589,6 +589,11 @@ class User < ActiveRecord::Base end def set_projects_limit + # `User.select(:id)` raises + # `ActiveModel::MissingAttributeError: missing attribute: projects_limit` + # without this safeguard! + return unless self.has_attribute?(:projects_limit) + connection_default_value_defined = new_record? && !projects_limit_changed? return unless self.projects_limit.nil? || connection_default_value_defined @@ -902,7 +907,7 @@ class User < ActiveRecord::Base if domain_matches?(allowed_domains, self.email) valid = true else - error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}" + error = "domain is not authorized for sign-up" valid = false end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index be25c750d67..be4721d7a51 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -40,7 +40,6 @@ class ProjectPolicy < BasePolicy can! :read_milestone can! :read_project_snippet can! :read_project_member - can! :read_merge_request can! :read_note can! :create_project can! :create_issue @@ -63,6 +62,7 @@ class ProjectPolicy < BasePolicy can! :read_pipeline can! :read_environment can! :read_deployment + can! :read_merge_request end # Permissions given when an user is team member of a project @@ -98,7 +98,6 @@ class ProjectPolicy < BasePolicy can! :admin_milestone can! :admin_project_snippet can! :admin_project_member - can! :admin_merge_request can! :admin_note can! :admin_wiki can! :admin_project @@ -118,6 +117,7 @@ class ProjectPolicy < BasePolicy can! :read_container_image can! :build_download_code can! :build_read_container_image + can! :read_merge_request end def owner_access! @@ -139,11 +139,18 @@ class ProjectPolicy < BasePolicy def team_access!(user) access = project.team.max_member_access(user.id) - guest_access! if access >= Gitlab::Access::GUEST - reporter_access! if access >= Gitlab::Access::REPORTER - team_member_reporter_access! if access >= Gitlab::Access::REPORTER - developer_access! if access >= Gitlab::Access::DEVELOPER - master_access! if access >= Gitlab::Access::MASTER + return if access < Gitlab::Access::GUEST + guest_access! + + return if access < Gitlab::Access::REPORTER + reporter_access! + team_member_reporter_access! + + return if access < Gitlab::Access::DEVELOPER + developer_access! + + return if access < Gitlab::Access::MASTER + master_access! end def archived_access! diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 0c208150fb8..1a2bad77a02 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -56,9 +56,8 @@ class BaseService result end - def success - { - status: :success - } + def success(pass_back = {}) + pass_back[:status] = :success + pass_back end end diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb deleted file mode 100644 index b2069ca825a..00000000000 --- a/app/services/boards/base_service.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Boards - class BaseService < ::BaseService - delegate :board, to: :project - end -end diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 072a0749285..9bdd7b6f0cf 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -1,16 +1,21 @@ module Boards - class CreateService < Boards::BaseService + class CreateService < BaseService def execute - create_board! unless project.board.present? - project.board + if project.boards.empty? + create_board! + else + project.boards.first + end end private def create_board! - project.create_board - project.board.lists.create(list_type: :backlog) - project.board.lists.create(list_type: :done) + board = project.boards.create + board.lists.create(list_type: :backlog) + board.lists.create(list_type: :done) + + board end end end diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb new file mode 100644 index 00000000000..c0d7ff5b585 --- /dev/null +++ b/app/services/boards/issues/create_service.rb @@ -0,0 +1,23 @@ +module Boards + module Issues + class CreateService < BaseService + def execute + create_issue(params.merge(label_ids: [list.label_id])) + end + + private + + def board + @board ||= project.boards.find(params.delete(:board_id)) + end + + def list + @list ||= board.lists.find(params.delete(:list_id)) + end + + def create_issue(params) + ::Issues::CreateService.new(project, current_user, params).execute + end + end + end +end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 34efd09ed9f..fd4a462c7b2 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -1,6 +1,6 @@ module Boards module Issues - class ListService < Boards::BaseService + class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless list.movable? @@ -10,6 +10,10 @@ module Boards private + def board + @board ||= project.boards.find(params[:board_id]) + end + def list @list ||= board.lists.find(params[:id]) end @@ -36,12 +40,7 @@ module Boards end def set_state - params[:state] = - case list.list_type.to_sym - when :backlog then 'opened' - when :done then 'closed' - else 'all' - end + params[:state] = list.done? ? 'closed' : 'opened' end def board_label_ids diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 84dc3f70e76..96554a92a02 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -1,6 +1,6 @@ module Boards module Issues - class MoveService < Boards::BaseService + class MoveService < BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) return false unless valid_move? @@ -10,6 +10,10 @@ module Boards private + def board + @board ||= project.boards.find(params[:board_id]) + end + def valid_move? moving_from_list.present? && moving_to_list.present? && moving_from_list != moving_to_list @@ -49,7 +53,7 @@ module Boards if moving_to_list.movable? moving_from_list.label_id else - board.lists.movable.pluck(:label_id) + project.boards.joins(:lists).merge(List.movable).pluck(:label_id) end Array(label_ids).compact diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb new file mode 100644 index 00000000000..84f1fc3a4e2 --- /dev/null +++ b/app/services/boards/list_service.rb @@ -0,0 +1,14 @@ +module Boards + class ListService < BaseService + def execute + create_board! if project.boards.empty? + project.boards + end + + private + + def create_board! + Boards::CreateService.new(project, current_user).execute + end + end +end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index b1887820bd4..abc7aeece39 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -1,23 +1,23 @@ module Boards module Lists - class CreateService < Boards::BaseService - def execute + class CreateService < BaseService + def execute(board) List.transaction do label = project.labels.find(params[:label_id]) - position = next_position + position = next_position(board) - create_list(label, position) + create_list(board, label, position) end end private - def next_position + def next_position(board) max_position = board.lists.movable.maximum(:position) max_position.nil? ? 0 : max_position.succ end - def create_list(label, position) + def create_list(board, label, position) board.lists.create(label: label, list_type: :label, position: position) end end diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index 25da3bfb56d..f986e05944c 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -1,9 +1,11 @@ module Boards module Lists - class DestroyService < Boards::BaseService + class DestroyService < BaseService def execute(list) return false unless list.destroyable? + @board = list.board + list.with_lock do decrement_higher_lists(list) remove_list(list) @@ -12,6 +14,8 @@ module Boards private + attr_reader :board + def decrement_higher_lists(list) board.lists.movable.where('position > ?', list.position) .update_all('position = position - 1') diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index 1c48b9786e4..d8048f1c67e 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -1,11 +1,11 @@ module Boards module Lists - class GenerateService < Boards::BaseService - def execute + class GenerateService < BaseService + def execute(board) return false unless board.lists.movable.empty? List.transaction do - label_params.each { |params| create_list(params) } + label_params.each { |params| create_list(board, params) } end true @@ -13,9 +13,9 @@ module Boards private - def create_list(params) + def create_list(board, params) label = find_or_create_label(params) - Lists::CreateService.new(project, current_user, label_id: label.id).execute + Lists::CreateService.new(project, current_user, label_id: label.id).execute(board) end def find_or_create_label(params) @@ -25,10 +25,8 @@ module Boards def label_params [ - { name: 'Development', color: '#5CB85C' }, - { name: 'Testing', color: '#F0AD4E' }, - { name: 'Production', color: '#FF5F00' }, - { name: 'Ready', color: '#FF0000' } + { name: 'To Do', color: '#F0AD4E' }, + { name: 'Doing', color: '#5CB85C' } ] end end diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb new file mode 100644 index 00000000000..c579ed4c869 --- /dev/null +++ b/app/services/boards/lists/list_service.rb @@ -0,0 +1,9 @@ +module Boards + module Lists + class ListService < BaseService + def execute(board) + board.lists + end + end + end +end diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb index 020ff69f4a7..f2a68865f7b 100644 --- a/app/services/boards/lists/move_service.rb +++ b/app/services/boards/lists/move_service.rb @@ -1,7 +1,8 @@ module Boards module Lists - class MoveService < Boards::BaseService + class MoveService < BaseService def execute(list) + @board = list.board @old_position = list.position @new_position = params[:position] @@ -16,7 +17,7 @@ module Boards private - attr_reader :old_position, :new_position + attr_reader :board, :old_position, :new_position def valid_move? new_position.present? && new_position != old_position && diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 36c93dddadb..d3dd30b2588 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -16,6 +16,8 @@ module Ci process_stage(index) end + @pipeline.update_status + # Return a flag if a when builds got enqueued new_builds.flatten.any? end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 6d6075628af..5e8fafca98c 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,7 +3,7 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch) + def execute(source_project, source_branch, target_project, target_branch, straight: false) source_commit = source_project.commit(source_branch) return unless source_commit @@ -23,9 +23,10 @@ class CompareService raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, - source_sha + source_sha, + straight ) - Compare.new(raw_compare, target_project) + Compare.new(raw_compare, target_project, straight: straight) end end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index c87542e57a2..923de58b244 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -2,31 +2,42 @@ require_relative 'base_service' class CreateDeploymentService < BaseService def execute(deployable = nil) - environment = find_or_create_environment + return unless executable? - if close? - environment.close - return + ActiveRecord::Base.transaction do + @deployable = deployable + @environment = prepare_environment + + if close? + @environment.close + return + end + + @environment.reopen + + deploy.tap do |deployment| + deployment.update_merge_request_metrics! + end end + end + + private - environment.reopen + def executable? + project && name.present? + end - deployment = project.deployments.create( - environment: environment, + def deploy + project.deployments.create( + environment: @environment, ref: params[:ref], tag: params[:tag], sha: params[:sha], user: current_user, - deployable: deployable - ) - - deployment.update_merge_request_metrics! - deployment + deployable: @deployable) end - private - - def find_or_create_environment + def prepare_environment project.environments.find_or_create_by(name: expanded_name) do |environment| environment.external_url = expanded_url end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index e8465729d06..9bd4bd464f7 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -27,8 +27,9 @@ module Files create_target_branch end - if commit - success + result = commit + if result + success(result: result) else error('Something went wrong. Your changes were not committed') end @@ -42,6 +43,12 @@ module Files @source_branch != @target_branch || @source_project != @project end + def file_has_changed? + return false unless @last_commit_sha && last_commit + + @last_commit_sha != last_commit.sha + end + def raise_error(message) raise ValidationError.new(message) end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb new file mode 100644 index 00000000000..d28912e1301 --- /dev/null +++ b/app/services/files/multi_service.rb @@ -0,0 +1,124 @@ +require_relative "base_service" + +module Files + class MultiService < Files::BaseService + class FileChangedError < StandardError; end + + def commit + repository.multi_action( + user: current_user, + branch: @target_branch, + message: @commit_message, + actions: params[:actions], + author_email: @author_email, + author_name: @author_name + ) + end + + private + + def validate + super + + params[:actions].each_with_index do |action, index| + unless action[:file_path].present? + raise_error("You must specify a file_path.") + end + + regex_check(action[:file_path]) + regex_check(action[:previous_path]) if action[:previous_path] + + if project.empty_repo? && action[:action] != :create + raise_error("No files to #{action[:action]}.") + end + + validate_file_exists(action) + + case action[:action] + when :create + validate_create(action) + when :update + validate_update(action) + when :delete + validate_delete(action) + when :move + validate_move(action, index) + else + raise_error("Unknown action type `#{action[:action]}`.") + end + end + end + + def validate_file_exists(action) + return if action[:action] == :create + + file_path = action[:file_path] + file_path = action[:previous_path] if action[:action] == :move + + blob = repository.blob_at_branch(params[:branch_name], file_path) + + unless blob + raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.") + end + end + + def last_commit + Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path) + end + + def regex_check(file) + if file =~ Gitlab::Regex.directory_traversal_regex + raise_error( + 'Your changes could not be committed, because the file name, `' + + file + + '` ' + + Gitlab::Regex.directory_traversal_regex_message + ) + end + + unless file =~ Gitlab::Regex.file_path_regex + raise_error( + 'Your changes could not be committed, because the file name, `' + + file + + '` ' + + Gitlab::Regex.file_path_regex_message + ) + end + end + + def validate_create(action) + return if project.empty_repo? + + if repository.blob_at_branch(params[:branch_name], action[:file_path]) + raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") + end + end + + def validate_delete(action) + end + + def validate_move(action, index) + if action[:previous_path].nil? + raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.") + end + + blob = repository.blob_at_branch(params[:branch_name], action[:file_path]) + + if blob + raise_error("Move destination `#{action[:file_path]}` already exists.") + end + + if action[:content].nil? + blob = repository.blob_at_branch(params[:branch_name], action[:previous_path]) + blob.load_all_data!(repository) if blob.truncated? + params[:actions][index][:content] = blob.data + end + end + + def validate_update(action) + if file_has_changed? + raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") + end + end + end +end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 9e9b5b63f26..c17fdb8d1f1 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -23,12 +23,6 @@ module Files end end - def file_has_changed? - return false unless @last_commit_sha && last_commit - - @last_commit_sha != last_commit.sha - end - def last_commit @last_commit ||= Gitlab::Git::Commit. last_for_path(@source_project.repository, @source_branch, @file_path) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c499427605a..e8415862de5 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -63,13 +63,12 @@ class GitPushService < BaseService protected def update_merge_requests - @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) + UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) EventCreateService.new.push(@project, current_user, build_push_data) - SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) - Ci::CreatePipelineService.new(project, current_user, build_push_data).execute + Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute ProjectCacheWorker.perform_async(@project.id) end @@ -148,16 +147,6 @@ class GitPushService < BaseService push_commits) end - def build_push_data_system_hook - @push_data_system ||= Gitlab::DataBuilder::Push.build( - @project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - []) - end - def push_to_existing_branch? # Return if this is not a push to a branch (e.g. new commits) Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev]) diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 566049525cb..d572a928a42 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -2,14 +2,14 @@ module MergeRequests class AddTodoWhenBuildFailsService < MergeRequests::BaseService # Adds a todo to the parent merge_request when a CI build fails def execute(commit_status) - each_merge_request(commit_status) do |merge_request| + commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_failed(merge_request) end end # Closes any pending build failed todos for the parent MRs when a build is retried def close(commit_status) - each_merge_request(commit_status) do |merge_request| + commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_retried(merge_request) end end diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb new file mode 100644 index 00000000000..f636e5fec4f --- /dev/null +++ b/app/services/merge_requests/assign_issues_service.rb @@ -0,0 +1,35 @@ +module MergeRequests + class AssignIssuesService < BaseService + def assignable_issues + @assignable_issues ||= begin + if current_user == merge_request.author + closes_issues.select do |issue| + !issue.assignee_id? && can?(current_user, :admin_issue, issue) + end + else + [] + end + end + end + + def execute + assignable_issues.each do |issue| + Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue) + end + + { + count: assignable_issues.count + } + end + + private + + def merge_request + params[:merge_request] + end + + def closes_issues + @closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user) + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index d0d155b7ee1..58f69a41e14 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -42,28 +42,33 @@ module MergeRequests super(:merge_request) end - def merge_request_from(commit_status) - branches = commit_status.ref + def merge_requests_for(branch) + origin_merge_requests = @project.origin_merge_requests + .opened.where(source_branch: branch).to_a - # This is for ref-less builds - branches ||= @project.repository.branch_names_contains(commit_status.sha) + fork_merge_requests = @project.fork_merge_requests + .opened.where(source_branch: branch).to_a - return [] if branches.blank? + (origin_merge_requests + fork_merge_requests) + .uniq.select(&:source_project) + end - merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a - merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a + def pipeline_merge_requests(pipeline) + merge_requests_for(pipeline.ref).each do |merge_request| + next unless pipeline == merge_request.pipeline - merge_requests.uniq.select(&:source_project) + yield merge_request + end end - def each_merge_request(commit_status) - merge_request_from(commit_status).each do |merge_request| + def commit_status_merge_requests(commit_status) + merge_requests_for(commit_status.ref).each do |merge_request| pipeline = merge_request.pipeline next unless pipeline next unless pipeline.sha == commit_status.sha - yield merge_request, pipeline + yield merge_request end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index e57791f6818..404f75616b5 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -4,7 +4,7 @@ module MergeRequests merge_request = MergeRequest.new(params) # Set MR attributes - merge_request.can_be_created = false + merge_request.can_be_created = true merge_request.compare_commits = [] merge_request.source_project = project unless merge_request.source_project @@ -22,6 +22,12 @@ module MergeRequests return build_failed(merge_request, message) end + if merge_request.source_project == merge_request.target_project && + merge_request.target_branch == merge_request.source_branch + + return build_failed(merge_request, 'You must select different branches') + end + compare = CompareService.new.execute( merge_request.source_project, merge_request.source_branch, @@ -29,17 +35,8 @@ module MergeRequests merge_request.target_branch, ) - commits = compare.commits - - # At this point we decide if merge request can be created - # If we have at least one commit to merge -> creation allowed - if commits.present? - merge_request.compare_commits = commits - merge_request.can_be_created = true - merge_request.compare = compare - else - merge_request.can_be_created = false - end + merge_request.compare_commits = compare.commits + merge_request.compare = compare set_title_and_description(merge_request) end @@ -89,6 +86,8 @@ module MergeRequests end end + merge_request.title = merge_request.wip_title if commits.empty? + merge_request end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 4ad5fb08311..dc159de0058 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -18,12 +18,13 @@ module MergeRequests merge_request.save end - # Triggers the automatic merge of merge_request once the build succeeds - def trigger(commit_status) - each_merge_request(commit_status) do |merge_request, pipeline| + # Triggers the automatic merge of merge_request once the pipeline succeeds + def trigger(pipeline) + return unless pipeline.success? + + pipeline_merge_requests(pipeline) do |merge_request| next unless merge_request.merge_when_build_succeeds? next unless merge_request.mergeable? - next unless pipeline.success? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 9dbec49d163..a37cc3fdf21 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -15,7 +15,10 @@ module MergeRequests params.except!(:target_branch, :force_remove_source_branch) end - merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + if params[:force_remove_source_branch].present? + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + end + handle_wip_event(merge_request) update(merge_request) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index de8049b8e2e..72712afc07e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -475,10 +475,12 @@ class NotificationService end def reject_users_without_access(recipients, target) - return recipients unless target.is_a?(Issue) + return recipients unless target.is_a?(Issuable) + + ability = :"read_#{target.to_ability_name}" recipients.select do |user| - user.can?(:read_issue, target) + user.can?(ability, target) end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 1725a30fae5..e4ae3dec8aa 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -122,7 +122,12 @@ module SlashCommands command :label do |labels_param| label_ids = find_label_ids(labels_param) - @updates[:add_label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:add_label_ids] ||= [] + @updates[:add_label_ids] += label_ids + + @updates[:add_label_ids].uniq! + end end desc 'Remove all or specific label(s)' @@ -136,7 +141,12 @@ module SlashCommands if labels_param.present? label_ids = find_label_ids(labels_param) - @updates[:remove_label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:remove_label_ids] ||= [] + @updates[:remove_label_ids] += label_ids + + @updates[:remove_label_ids].uniq! + end else @updates[:label_ids] = [] end @@ -152,7 +162,12 @@ module SlashCommands command :relabel do |labels_param| label_ids = find_label_ids(labels_param) - @updates[:label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:label_ids] ||= [] + @updates[:label_ids] += label_ids + + @updates[:label_ids].uniq! + end end desc 'Add a todo' diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 776530ac0a5..f8e6b2ef094 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -273,12 +273,12 @@ class TodoService end def reject_users_without_access(users, project, target) - if target.is_a?(Note) && target.for_issue? + if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?) target = target.noteable end - if target.is_a?(Issue) - select_users(users, :read_issue, target) + if target.is_a?(Issuable) + select_users(users, :"read_#{target.to_ability_name}", target) else select_users(users, :read_project, project) end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 4dc3b2ab9a0..2821ecf0a88 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -24,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator projects public repository + robots.txt s search services diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 56bf6194914..05f3d9a3b50 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -21,7 +21,7 @@ %td %strong.subheading.visible-xs-block.visible-sm-block Message .message - = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) + = markdown_field(abuse_report, :message) %td - if user = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml index 6c51639b840..acbe17036f7 100644 --- a/app/views/admin/appearances/preview.html.haml +++ b/app/views/admin/appearances/preview.html.haml @@ -1,9 +1,12 @@ -- page_title "Preview | Appearance" += render 'devise/shared/tab_single', tab_title: 'Sign in preview' .login-box - .login-heading - %h3 Existing user? Sign in - %form - = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email" - = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password" - = button_tag "Sign in", class: "btn-create btn" + %form.show-gl-field-errors + .form-group + = label_tag :login + = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.' + .form-group + = label_tag :password + = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.' + .form-group + = button_tag "Sign in", class: "btn-create btn" diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0d79ca7dc52..c4c68cd7891 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -221,7 +221,11 @@ %fieldset %legend Metrics %p - These settings require a restart to take effect. + Setup InfluxDB to measure a wide variety of statistics like the time spent + in running SQL queries. These settings require 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') .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index f952d2e9aa1..3132d157f29 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -1,7 +1,10 @@ .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } = icon('bullhorn') .js-broadcast-message-preview - = render_broadcast_message(@broadcast_message.message.presence || "Your message here") + - if @broadcast_message.message.present? + = render_broadcast_message(@broadcast_message) + - else + = "Your message here" = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_errors(@broadcast_message) diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml index fbc9453c72e..c72e59640d7 100644 --- a/app/views/admin/broadcast_messages/preview.js.haml +++ b/app/views/admin/broadcast_messages/preview.js.haml @@ -1 +1 @@ -$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}"); +$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}"); diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 77a11e49e20..adfa1eaafc9 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -23,4 +23,4 @@ - if group.description.present? .description - = markdown(group.description, pipeline: :description) + = markdown_field(group, :description) diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index f417b2e44a4..be224d66855 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,7 +1,7 @@ %li{id: dom_id(label)} .label-row = render_colored_label(label, tooltip: false) - = markdown(label.description, pipeline: :single_line) + = markdown_field(label, :description) .pull-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 1e755785d90..339cfc613fe 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -87,7 +87,7 @@ - if project.description.present? .description - = markdown(project.description, pipeline: :description) + = markdown_field(project, :description) = paginate @projects, theme: 'gitlab' - else diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index a5e82e55cc1..10fea1996aa 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -71,7 +71,7 @@ .col-md-6 %h4 Recent builds served by this Runner - %table.table.builds.runner-builds + %table.table.ci-table.runner-builds %thead %tr %th Build diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 9d31f31c639..2a0302638ba 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -55,7 +55,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to todos_filter_path(sort: sort_value_priority) do diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 73c3a3dd2eb..20cd7b0179d 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -3,9 +3,9 @@ Almost there... %p.lead Please check your email to confirm your account -- if after_sign_up_text.present? +- if current_application_settings.after_sign_up_text.present? .well-confirmation.text-center - = markdown(after_sign_up_text) + = markdown_field(current_application_settings, :after_sign_up_text) %p.confirmation-content.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index 970ba147111..5d25dd398d6 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -1,14 +1,14 @@ += render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions' .login-box - .login-heading - %h3 Resend confirmation instructions .login-body - = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| + = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! - .clearfix.append-bottom-20 - = f.email_field :email, placeholder: 'Email', class: "form-control", required: true + .form-group + = f.label :email + = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.' .clearfix - = f.submit "Resend confirmation instructions", class: 'btn btn-success' + = f.submit "Resend", class: 'btn btn-success' .clearfix.prepend-top-20 = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 56048e99c17..b518fae7c95 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -1,19 +1,21 @@ += render 'devise/shared/tab_single', tab_title:'Change your password' .login-box - .login-heading - %h3 Change your password .login-body - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! = f.hidden_field :reset_password_token - %div - = f.password_field :password, class: "form-control top", placeholder: "New password", required: true - %div - = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true + .form-group + = f.label 'New password', for: :password + = f.password_field :password, class: "form-control top", required: true, title: 'This field is required' + .form-group + = f.label 'Confirm new password', for: :password_confirmation + = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true .clearfix = f.submit "Change your password", class: "btn btn-primary" .clearfix.prepend-top-20 %p - = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) - = render 'devise/shared/sign_in_link' + %span.light Didn't receive a confirmation email? + = link_to "Request a new one", new_confirmation_path(resource_name) += render 'devise/shared/sign_in_link' diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 535e85869e5..1fcfd06419a 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -1,12 +1,12 @@ += render 'devise/shared/tab_single', tab_title: 'Reset Password' .login-box - .login-heading - %h3 Reset password .login-body - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! - .clearfix.append-bottom-20 - = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true + .form-group + = f.label :email + = f.email_field :email, class: "form-control", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.' .clearfix = f.submit "Reset password", class: "btn-primary btn" diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 9f5520603cd..a96b579c593 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,6 +1,10 @@ -= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off" - = f.password_field :password, class: "form-control bottom", placeholder: "Password" += form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f| + %div.form-group + = f.label "Username or email", for: :login + = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." + %div.form-group + = f.label :password + = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "user_remember_me"} @@ -8,5 +12,5 @@ %span Remember me .pull-right = link_to "Forgot your password?", new_password_path(resource_name) - %div + %div.submit-container = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index b7d3acac2b1..e82a08cdb0c 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,6 +1,10 @@ -= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do - = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"} - = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} += form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do + .form-group + = label_tag 'Username or email', for: :username + = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } + .form-group + = label_tag :password + = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "remember_me"} diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 2ef383960f4..b26efbb4535 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,6 +1,10 @@ -= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do - = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} - = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} += form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do + .form-group + = label_tag "#{server['label']} Login", for: :username + = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } + .form-group + = label_tag :password + = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "remember_me"} diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 28194506acc..fa8e7979461 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,19 +1,22 @@ - page_title "Sign in" %div - - if signin_enabled? || ldap_enabled? || crowd_enabled? - = render 'devise/shared/signin_box' + - if form_based_providers.any? + = render 'devise/shared/tabs_ldap' + - else + = render 'devise/shared/tabs_normal' + .tab-content + - if signin_enabled? || ldap_enabled? || crowd_enabled? + = render 'devise/shared/signin_box' - -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? - .clearfix.prepend-top-20 - = render 'devise/shared/omniauth_box' - - -# Signup only makes sense if you can also sign-in - - if signin_enabled? && signup_enabled? - .prepend-top-20 + -# Signup only makes sense if you can also sign-in + - if signin_enabled? && signup_enabled? = render 'devise/shared/signup_box' -# Show a message if none of the mechanisms above are enabled - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?) %div No authentication methods configured. + + - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? + .clearfix + = render 'devise/shared/omniauth_box' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index e623f7cff88..0e865b807c1 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -3,20 +3,19 @@ = page_specific_javascript_tag('u2f.js') %div + = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' .login-box - .login-heading - %h3 Two-Factor Authentication .login-body - if @user.two_factor_otp_enabled? - %h5 Authenticate via Two-Factor App - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' - %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. - .prepend-top-20 - = f.submit "Verify code", class: "btn btn-save" + .form-group + = f.label 'Two-Factor Authentication code', name: :otp_attempt + = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' + %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" - if @user.two_factor_u2f_enabled? - %hr = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name } diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 2e7da2747d0..8908b64cdac 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,8 +1,9 @@ -%p - %span.light - Sign in with - - providers = enabled_button_based_providers - - providers.each do |provider| +%div.omniauth-container + %p %span.light - - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + Sign in with + - providers = enabled_button_based_providers + - providers.each do |provider| + %span.light + - has_icon = provider_has_icon?(provider) + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml index fafc4b82f53..289bf40f3de 100644 --- a/app/views/devise/shared/_sign_in_link.html.haml +++ b/app/views/devise/shared/_sign_in_link.html.haml @@ -1,5 +1,4 @@ %p %span.light Already have login and password? - %strong = link_to "Sign in", new_session_path(resource_name) diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 2c15e2c4891..810dd5ab687 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,32 +1,15 @@ -.login-box - - if signup_enabled? - .login-heading - %h3 Existing user? Sign in - - else - .login-heading - %h3 Sign in +#login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' } .login-body - if form_based_providers.any? - %ul.nav-links - - if crowd_enabled? - %li.active - = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' - - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero? && !crowd_enabled?)} - = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' - - if signin_enabled? - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' - .tab-content - - if crowd_enabled? - %div.tab-pane.active{id: "tab-crowd"} - = render 'devise/sessions/new_crowd' - - @ldap_servers.each_with_index do |server, i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} - = render 'devise/sessions/new_ldap', server: server - - if signin_enabled? - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' + - if crowd_enabled? + %div.tab-pane.active{id: "tab-crowd"} + = render 'devise/sessions/new_crowd' + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} + = render 'devise/sessions/new_ldap', server: server + - if signin_enabled? + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' - elsif signin_enabled? = render 'devise/sessions/new_base' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 905a8dbcd84..d0bbcf3115e 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,29 +1,30 @@ -.login-box - - if signin_enabled? - .login-heading - %h3 New user? Create an account - - else - .login-heading - %h3 Create an account +#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' } .login-body - = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name)) do |f| + = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user show-gl-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = devise_error_messages! - %div - = f.text_field :name, class: "form-control top", placeholder: "Name", required: true - %div - = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true - %div - = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true + %div.form-group + = f.label :name + = f.text_field :name, class: "form-control top", required: true, title: "This field is required." + %div.username.form-group + = f.label :username + = f.text_field :username, class: "form-control middle no-gl-field-error", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.' + %p.validation-error.hide Username is already taken. + %p.validation-success.hide Username is available. + %p.validation-pending.hide Checking username availability... + %div.form-group + = f.label :email + = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters" + = f.label :password + = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." + %p.gl-field-hint Minimum length is #{@minimum_password_length} characters %div - if current_application_settings.recaptcha_enabled = recaptcha_tags %div - = f.submit "Sign up", class: "btn-create btn" - -.clearfix.prepend-top-20 + = f.submit "Register", class: "btn-register btn" +.clearfix.submit-container %p %span.light Didn't receive a confirmation email? = succeed '.' do diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml new file mode 100644 index 00000000000..f943d25e41a --- /dev/null +++ b/app/views/devise/shared/_tab_single.html.haml @@ -0,0 +1,3 @@ +%ul.nav-links.nav-tabs.new-session-tabs.single-tab + %li.active + %a= tab_title diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml new file mode 100644 index 00000000000..e276e91433a --- /dev/null +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -0,0 +1,10 @@ +%ul.new-session-tabs.nav-links.nav-tabs + - if crowd_enabled? + %li.active + = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i.zero? && !crowd_enabled?)} + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + - if signin_enabled? + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml new file mode 100644 index 00000000000..79b1d447a92 --- /dev/null +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -0,0 +1,5 @@ +%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} + %li.active{ role: 'presentation' } + %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in + %li{ role: 'presentation'} + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml index 49c087c0646..49b2f77111f 100644 --- a/app/views/devise/unlocks/new.html.haml +++ b/app/views/devise/unlocks/new.html.haml @@ -1,12 +1,12 @@ += render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions' .login-box - .login-heading - %h3 Resend unlock email .login-body - = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| + = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! - .clearfix.append-bottom-20 - = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off' + .form-group.append-bottom-20 + = f.label :email + = f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.' .clearfix = f.submit 'Resend unlock instructions', class: 'btn btn-success' diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index b8248a80a27..a1b39d9e1a0 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -23,7 +23,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to explore_groups_path(sort: sort_value_recently_created) do diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 132bbe26fe0..4cff14b096b 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -7,7 +7,7 @@ = visibility_level_label(params[:visibility_level].to_i) - else Any - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(visibility_level: nil) do @@ -27,7 +27,7 @@ = params[:tag] - else Any - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(tag: nil) do diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index 2fb3190ab11..b185b81db7f 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -1,27 +1,22 @@ -= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10 - = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) - .help-block += form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f| + .row + .col-md-4.col-lg-6 + = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) + .help-block.append-bottom-10 Search for users by name, username, or email, or invite new ones using their email address. - .form-group - = f.label :access_level, "Group Access", class: 'control-label' - .col-sm-10 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2" - .help-block - Read more about role permissions - %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .col-md-3.col-lg-2 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions - .form-group - = f.label :expires_at, 'Access expiration date', class: 'control-label' - .col-sm-10 + .col-md-3.col-lg-2 .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - .help-block + .help-block.append-bottom-10 On this date, the user(s) will automatically lose access to this group and all of its projects. - .form-actions - = f.submit 'Add users to group', class: "btn btn-create" + .col-md-2 + = f.submit 'Add to group', class: "btn btn-create btn-block" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index f789796e942..ebf9aca7700 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,35 +1,31 @@ - page_title "Members" -.group-members-page.prepend-top-default +.project-members-page.prepend-top-default + %h4 + Members + %hr - if can?(current_user, :admin_group_member, @group) - .panel.panel-default - .panel-heading - Add new user to group - .panel-body - %p.light - Members of group have access to all group projects. - .new-group-member-holder - = render "new_group_member" + .project-members-new.append-bottom-default + %p.clearfix + Add new user to + %strong= @group.name + = render "new_group_member" = render 'shared/members/requests', membership_source: @group, requesters: @requesters + .append-bottom-default.clearfix + %h5.member.existing-title + Existing users + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") .panel.panel-default .panel-heading + Users with access to %strong #{@group.name} - group members %span.badge= @members.total_count - .controls - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } - = button_tag class: 'btn', title: 'Search' do - = icon("search") %ul.content-list = render partial: 'shared/members/member', collection: @members, as: :member = paginate @members, theme: 'gitlab' - -:javascript - $('form.member-search-form').on('submit', function(event) { - event.preventDefault(); - Turbolinks.visit(this.action + '?' + $(this).serialize()); - }); diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index 3be7ed8432c..de8f53b6b52 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,3 @@ :plain - $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); - new gl.MemberExpirationDate(); + var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); + $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 31db6ee0cad..fab61f447c2 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -21,7 +21,7 @@ - if @group.description.present? .cover-desc.description - = markdown(@group.description, pipeline: :description) + = markdown_field(@group, :description) %div.groups-header{ class: container_class } .top-area diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 57601ae9be0..31631887317 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -20,7 +20,7 @@ Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. - if current_application_settings.help_page_text.present? %hr - = markdown(current_application_settings.help_page_text) + = markdown_field(current_application_settings, :help_page_text) %hr diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 67ff4b272b9..e138ebab018 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,7 +1,8 @@ - project = @target_project || @project - noteable_type = @noteable.class if @noteable.present? -:javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" - GitLab.GfmAutoComplete.cachedData = undefined; - GitLab.GfmAutoComplete.setup(); +- if project + :javascript + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" + GitLab.GfmAutoComplete.cachedData = undefined; + GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 15a94ac23c5..6c2285fa2b6 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -11,3 +11,4 @@ = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body + = render "layouts/init_auto_complete" if @gfm_form diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 3d28eec84ef..6922f1e153f 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,36 +1,37 @@ !!! 5 -%html{ lang: "en"} +%html{ lang: "en", class: "devise-layout-html"} = render "layouts/head" - %body.ui_charcoal.login-page.application.navless - = Gon::Base.render_data - = render "layouts/header/empty" - = render "layouts/broadcast" - .container.navless-container - .content - = render "layouts/flash" - .row - .col-sm-5.pull-right - = yield - .col-sm-7.brand-holder.pull-left - %h1 - = brand_title - - if brand_item - = brand_image - = brand_text - - else - %h3 Open source software to collaborate on code + %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }} + .page-wrap + = Gon::Base.render_data + = render "layouts/header/empty" + = render "layouts/broadcast" + .container.navless-container + .content + = render "layouts/flash" + .row + .col-sm-5.pull-right.new-session-forms-container + = yield + .col-sm-7.brand-holder.pull-left + %h1 + = brand_title + - if brand_item + = brand_image + = brand_text + - else + %h3 Open source software to collaborate on code - %p - Manage git repositories with fine grained access controls that keep your code secure. - Perform code reviews and enhance collaboration with merge requests. - Each project can also have an issue tracker and a wiki. + %p + Manage Git repositories with fine-grained access controls that keep your code secure. + Perform code reviews and enhance collaboration with merge requests. + Each project can also have an issue tracker and a wiki. - - if extra_sign_in_text.present? - = markdown(extra_sign_in_text) + - if current_application_settings.sign_in_text.present? + = markdown_field(current_application_settings, :sign_in_text) - %hr - .container - .footer-links - = link_to "Explore", explore_root_path - = link_to "Help", help_path - = link_to "About GitLab", "https://about.gitlab.com/" + %hr.footer-fixed + .container.footer-container + .footer-links + = link_to "Explore", explore_root_path + = link_to "Help", help_path + = link_to "About GitLab", "https://about.gitlab.com/" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 237280872f1..7faa8bded86 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -41,7 +41,7 @@ %li.header-user.dropdown = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" - %span.caret + = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul %li diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index e44a2bfed9d..99a58bbb676 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -116,4 +116,4 @@ -# Shortcut to issue boards %li.hidden - = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + = link_to 'Issue Boards', namespace_project_boards_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index c80f22457b4..e2e974ba072 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -86,11 +86,11 @@ = f.label :username, "Path", class: "label-light" .input-group .input-group-addon - = "#{root_url}u/" + = root_url = f.text_field :username, required: true, class: 'form-control' .help-block Current path: - = "#{root_url}u/#{current_user.username}" + = "#{root_url}#{current_user.username}" .prepend-top-default = f.button class: "btn btn-warning", type: "submit" do = icon "spinner spin", class: "hidden loading-username" diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 8ef31ca3bda..5590198a20e 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -9,7 +9,7 @@ .project-home-desc - if @project.description.present? - = markdown(@project.description, pipeline: :description) + = markdown_field(@project, :description) - if forked_from_project = @project.forked_from_project %p diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index cb97181b9e1..0c8241053e7 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,3 +1,4 @@ +- @gfm_form = true - supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' @@ -7,6 +8,3 @@ = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') - -- content_for :scripts_body do - = render "layouts/init_auto_complete" if current_user && (@target_project || @project) diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 5a98e258b22..dfb96305f48 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,45 +1,48 @@ +- @no_container = true - page_title "Blame", @blob.path, @ref += render "projects/commits/head" -%h3.page-title Blame view +%div{ class: container_class } + %h3.page-title Blame view -#blob-content-holder.tree-holder - .file-holder - .file-title - = blob_icon @blob.mode, @blob.name - %strong - = @path - %small= number_to_human_size @blob.size - .file-actions - = render "projects/blob/actions" - .table-responsive.file-content.blame.code.js-syntax-highlight - %table - - current_line = 1 - - @blame_groups.each do |blame_group| - %tr - %td.blame-commit - .commit - - commit = blame_group[:commit] - = author_avatar(commit, size: 36) - .commit-row-title - %strong - = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" - .pull-right - = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" - - .light - = commit_author_link(commit, avatar: false) - authored - #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} - %td.line-numbers - - line_count = blame_group[:lines].count - - (current_line...(current_line + line_count)).each do |i| - %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} - = icon("link") - = i - \ - - current_line += line_count - %td.lines - %pre.code.highlight - %code - - blame_group[:lines].each do |line| - #{line} + #blob-content-holder.tree-holder + .file-holder + .file-title + = blob_icon @blob.mode, @blob.name + %strong + = @path + %small= number_to_human_size @blob.size + .file-actions + = render "projects/blob/actions" + .table-responsive.file-content.blame.code.js-syntax-highlight + %table + - current_line = 1 + - @blame_groups.each do |blame_group| + %tr + %td.blame-commit + .commit + - commit = blame_group[:commit] + = author_avatar(commit, size: 36) + .commit-row-title + %strong + = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" + .pull-right + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" + + .light + = commit_author_link(commit, avatar: false) + authored + #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + %td.line-numbers + - line_count = blame_group[:lines].count + - (current_line...(current_line + line_count)).each do |i| + %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + = icon("link") + = i + \ + - current_line += line_count + %td.lines + %pre.code.highlight + %code + - blame_group[:lines].each do |line| + #{line} diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 680e95ac6b5..2a0352a71b7 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,28 +1,31 @@ +- @no_container = true - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') += render "projects/commits/head" -- if @conflict - .alert.alert-danger - Someone edited the file the same time you did. Please check out - = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank" - and make sure your changes will not unintentionally remove theirs. +%div{ class: container_class } + - if @conflict + .alert.alert-danger + Someone edited the file the same time you did. Please check out + = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank" + and make sure your changes will not unintentionally remove theirs. -.file-editor - %ul.nav-links.no-bottom.js-edit-mode - %li.active - = link_to '#editor' do - Edit File + .file-editor + %ul.nav-links.no-bottom.js-edit-mode + %li.active + = link_to '#editor' do + Edit File - %li - = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do - = editing_preview_title(@blob.name) + %li + = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do + = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do - = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data - = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" - = hidden_field_tag 'last_commit_sha', @last_commit_sha - = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do + = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data + = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" + = hidden_field_tag 'last_commit_sha', @last_commit_sha + = hidden_field_tag 'content', '', id: "file-content" + = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 73066150fb3..ba1502c97b6 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -12,8 +12,17 @@ %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } {{ list.title }} - %span.pull-right{ "v-if" => "list.type !== 'blank'" } - {{ list.issuesSize }} + .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" } + %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" } + {{ list.issuesSize }} + - if can?(current_user, :admin_issue, @project) + %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", + "@click" => "showNewIssueForm", + "v-if" => "list.type !== 'done'", + "aria-label" => "Add an issue", + "title" => "Add an issue", + data: { placement: "top", container: "body" } } + = icon("plus") - if can?(current_user, :admin_list, @project) %board-delete{ "inline-template" => true, ":list" => "list", @@ -26,12 +35,38 @@ ":issues" => "list.issues", ":loading" => "list.loading", ":disabled" => "disabled", + ":show-issue-form.sync" => "showIssueForm", ":issue-link-base" => "issueLinkBase" } .board-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") + - if can? current_user, :create_issue, @project + %board-new-issue{ "inline-template" => true, + ":list" => "list", + ":show-issue-form.sync" => "showIssueForm", + "v-show" => "list.type !== 'done' && showIssueForm" } + .card.board-new-issue-form + %form{ "@submit" => "submit($event)" } + .flash-container{ "v-if" => "error" } + .flash-alert + An error occured. Please try again. + %label.label-light{ ":for" => "list.id + '-title'" } + Title + %input.form-control{ type: "text", + "v-model" => "title", + "v-el:input" => true, + ":id" => "list.id + '-title'" } + .clearfix.prepend-top-10 + %button.btn.btn-success.pull-left{ type: "submit", + ":disabled" => "title === ''", + "v-el:submit-button" => true } + Submit issue + %button.btn.btn-default.pull-right{ type: "button", + "@click" => "cancel" } + Cancel %ul.board-list{ "v-el:list" => true, "v-show" => "!loading", - ":data-board" => "list.id" } + ":data-board" => "list.id", + ":class" => "{ 'is-smaller': showIssueForm }" } = render "projects/boards/components/card" %li.board-list-count.text-center{ "v-if" => "showCount" } = icon("spinner spin", "v-show" => "list.loadingMore" ) diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index e8b60b54d80..d8f16022407 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -7,7 +7,7 @@ ":issue-link-base" => "issueLinkBase", ":disabled" => "disabled", "track-by" => "id" } - %li.card{ ":class" => "{ 'user-can-drag': !disabled }", + %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }", ":index" => "index" } %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") @@ -15,7 +15,7 @@ ":title" => "issue.title" } {{ issue.title }} .card-footer - %span.card-number + %span.card-number{ "v-if" => "issue.id" } = precede '#' do {{ issue.id }} %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", @@ -26,7 +26,7 @@ ":title" => "label.description", data: { container: 'body' } } {{ label.title }} - %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username", + %a.has-tooltip{ ":href" => "'/' + issue.assignee.username", ":title" => "'Assigned to ' + issue.assignee.name", "v-if" => "issue.assignee", data: { container: 'body' } } diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml new file mode 100644 index 00000000000..885f8e34b55 --- /dev/null +++ b/app/views/projects/boards/index.html.haml @@ -0,0 +1,16 @@ +- @no_container = true +- @content_class = "issue-boards-content" +- page_title "Boards" + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('boards/boards_bundle.js') + = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? + += render "projects/issues/head" + += render 'shared/issuable/filter', type: :boards + +.boards-list#board-app{ "v-cloak" => true, data: board_data } + .boards-app-loading.text-center{ "v-if" => "loading" } + = icon("spinner spin") + = render "projects/boards/components/board" diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml index edbbd3f3d2a..885f8e34b55 100644 --- a/app/views/projects/boards/show.html.haml +++ b/app/views/projects/boards/show.html.haml @@ -10,10 +10,7 @@ = render 'shared/issuable/filter', type: :boards -.boards-list#board-app{ "v-cloak" => true, - "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}", - "data-disabled" => "#{!can?(current_user, :admin_list, @project)}", - "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" } +.boards-list#board-app{ "v-cloak" => true, data: board_data } .boards-app-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") = render "projects/boards/components/board" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 5217b8bf028..4480b2f22c3 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -30,8 +30,8 @@ = render 'projects/buttons/download', project: @project, ref: branch.name - - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do + - if can?(current_user, :push_code, @project) + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: "btn btn-remove remove-row has-tooltip #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") - if branch.name != @repository.root_ref diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index e889f29c816..84f38575e84 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -15,7 +15,7 @@ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light = projects_sort_options_hash[@sort] - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_branches_path(sort: sort_value_name) do diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f5344091cae..966633f1f89 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -128,7 +128,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{class: ('active' if build == @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('right-arrow') + = icon('arrow-right') = ci_icon_for_status(build.status) %span - if build.name diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index f3747ba2a21..36294c89fa8 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -5,7 +5,7 @@ .nothing-here-block No builds to show - else .table-holder - %table.table.builds + %table.table.ci-table.builds-page %thead %tr %th Status diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index e4d41288aa6..b5e8b0bf6eb 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,56 +1,59 @@ +- @no_container = true - page_title "#{@build.name} (##{@build.id})", "Builds" - trace_with_state = @build.trace_with_state - header_title project_title(@project, "Builds", project_builds_path(@project)) += render "projects/pipelines/head", build_subnav: true -.build-page - = render "header" +%div{ class: container_class } + .build-page + = render "header" - - if @build.stuck? - - unless @build.any_runners_online? - .bs-callout.bs-callout-warning - %p - - if no_runners_for_project?(@build.project) - This build is stuck, because the project doesn't have any runners online assigned to it. - - elsif @build.tags.any? - This build is stuck, because you don't have any active runners online with any of these tags assigned to them: - - @build.tags.each do |tag| - %span.label.label-primary - = tag - - else - This build is stuck, because you don't have any active runners that can run this build. + - if @build.stuck? + - unless @build.any_runners_online? + .bs-callout.bs-callout-warning + %p + - if no_runners_for_project?(@build.project) + This build is stuck, because the project doesn't have any runners online assigned to it. + - elsif @build.tags.any? + This build is stuck, because you don't have any active runners online with any of these tags assigned to them: + - @build.tags.each do |tag| + %span.label.label-primary + = tag + - else + This build is stuck, because you don't have any active runners that can run this build. - %br - Go to - = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do - Runners page + %br + Go to + = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do + Runners page - .prepend-top-default - - if @build.active? - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - - if @build.erased? - .erased.alert.alert-warning - - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} - - else - #js-build-scroll.scroll-controls - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down - %pre.build-trace#build-trace - %code.bash.js-build-output - = icon("refresh spin", class: "js-build-refresh") + .prepend-top-default + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + - if @build.erased? + .erased.alert.alert-warning + - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by + Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} + - else + #js-build-scroll.scroll-controls + = link_to '#build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down + %pre.build-trace#build-trace + %code.bash.js-build-output + = icon("refresh spin", class: "js-build-refresh") - #down-build-trace + #down-build-trace -= render "sidebar" + = render "sidebar" -:javascript - new Build({ - page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}", - build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", - build_status: "#{@build.status}", - build_stage: "#{@build.stage}", - state1: "#{trace_with_state[:state]}" - }) + :javascript + new Build({ + page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}", + build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", + build_status: "#{@build.status}", + build_stage: "#{@build.stage}", + state1: "#{trace_with_state[:state]}" + }) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 22db33498f1..29d549a60f5 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -5,10 +5,10 @@ = custom_icon('icon_fork') %span Fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do = custom_icon('icon_fork') %span Fork %div.count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do = @project.forks_count diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 547bc0c9c19..017d3ff6af2 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -5,8 +5,10 @@ .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do - = render_status_with_link('build', subject.status) + %span.ci-status-icon + = render_status_with_link('build', subject.status) .ci-status-text= subject.name - else - = render_status_with_link('build', subject.status) + %span.ci-status-icon + = render_status_with_link('build', subject.status) = ci_icon_for_status(subject.status) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b87c7a485df..36eadbd2bf1 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -67,7 +67,7 @@ .btn-group %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play') - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li @@ -78,7 +78,7 @@ .btn-group %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} = icon("download") - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - artifacts.each do |build| %li diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 29d767e7769..6c82a4e5600 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -14,7 +14,7 @@ .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } %span.hidden-xs Options - %span.caret.commit-options-dropdown-caret + = icon('caret-down', class: ".commit-options-dropdown-caret") %ul.dropdown-menu.dropdown-menu-align-right %li.visible-xs-block.visible-sm-block = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do @@ -24,6 +24,8 @@ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) %li.clearfix = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) + %li.clearfix + = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header Download @@ -63,10 +65,10 @@ .commit-box.content-block %h3.commit-title - = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author + = markdown(@commit.title, pipeline: :single_line, author: @commit.author) - if @commit.description.present? %pre.commit-description - = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author)) + = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) :javascript $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 9258f4b3c25..d6916fb7f1a 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,45 +1,46 @@ -.row-content-block.build-content.middle-block.pipeline-actions - .pull-right - .btn.btn-grouped.btn-white.toggle-pipeline-btn - %span.toggle-btn-text Hide - %span pipeline graph - %span.caret - - if can?(current_user, :update_pipeline, pipeline.project) - - if pipeline.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post +.pipeline-graph-container + .row-content-block.build-content.middle-block.pipeline-actions + .pull-right + .btn.btn-grouped.btn-white.toggle-pipeline-btn + %span.toggle-btn-text Hide + %span pipeline graph + %span.caret + - if can?(current_user, :update_pipeline, pipeline.project) + - if pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post - - if pipeline.builds.running_or_pending.any? - = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + - if pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - .oneline.clearfix - - if defined?(pipeline_details) && pipeline_details - Pipeline - = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" - with - = pluralize pipeline.statuses.count(:id), "build" - - if pipeline.ref - for - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" - - if pipeline.duration - in - = time_interval_in_words pipeline.duration + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration -.row-content-block.build-content.middle-block.pipeline-graph - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + .row-content-block.build-content.middle-block.pipeline-graph.hidden + .pipeline-visualization + %ul.stage-column-list + - stages = pipeline.stages_with_latest_statuses + - stages.each do |stage, statuses| + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + = render "projects/commit/pipeline_stage", statuses: statuses - if pipeline.yaml_errors.present? @@ -55,7 +56,7 @@ \.gitlab-ci.yml not found in this commit .table-holder.pipeline-holder - %table.table.builds.pipeline + %table.table.ci-table.pipeline %thead %tr %th Status diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml index 23c5c51fbc2..289aa5178b1 100644 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -10,5 +10,5 @@ - else %li.build .curve - .build-content + .dropdown.inline.build-content = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 4e7a6f1af08..5d0d5ba0262 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,11 +1,13 @@ - group_status = CommitStatus.where(id: subject).status -= render_status_with_link('build', group_status) -.dropdown.inline - %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } - %span.ci-status-text - = name - %span.badge= subject.size - %ul.dropdown-menu.grouped-pipeline-dropdown - .arrow +%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %span.ci-status-icon + = render_status_with_link('build', group_status) + %span.ci-status-text + = name + %span.badge= subject.size +.dropdown-menu.grouped-pipeline-dropdown + .arrow + %ul - subject.each do |status| - = render "projects/#{status.to_partial_path}_pipeline", subject: status + %li + = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 998812793a2..640651e93f5 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -4,7 +4,7 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.builds + %table.table.ci-table %tbody %th Status %th Pipeline diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 2f051fb90e0..f9d7eac3542 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,7 +1,10 @@ +- @no_container = true - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" += render "projects/commits/head" -.prepend-top-default - = render "commit_box" +%div{ class: container_class } + .prepend-top-default + = render "commit_box" -= render "ci_menu" -= render "builds" + = render "ci_menu" + = render "builds" diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index ed44d86a687..cebf58d63df 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,14 +1,17 @@ +- @no_container = true - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description += render "projects/commits/head" -.prepend-top-default - = render "commit_box" -- if @commit.status - = render "ci_menu" -- else - %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs -= render "projects/notes/notes_with_form" -- if can_collaborate_with_project? - - %w(revert cherry-pick).each do |type| - = render "projects/commit/change", type: type, commit: @commit, title: @commit.title +%div{ class: container_class } + .prepend-top-default + = render "commit_box" + - if @commit.status + = render "ci_menu" + - else + %div.block-connector + = render "projects/diffs/diffs", diffs: @diffs + = render "projects/notes/notes_with_form" + - if can_collaborate_with_project? + - %w(revert cherry-pick).each do |type| + = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 389477d0927..fb48aef0559 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -33,7 +33,7 @@ - if commit.description? %pre.commit-row-description.js-toggle-content - = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) + = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) .commit-row-info = commit_author_link(commit, avatar: false, size: 24) diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml index 27d928c87a0..05fb37cdc0f 100644 --- a/app/views/projects/compare/_ref_dropdown.html.haml +++ b/app/views/projects/compare/_ref_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown-menu.dropdown-menu-selectable - = dropdown_title "Select branch/tag" - = dropdown_filter "Filter by branch/tag" + = dropdown_title "Select Git revision" + = dropdown_filter "Filter by Git revision" = dropdown_content = dropdown_loading diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index e9ff8e90dd5..45be6581cfc 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -4,7 +4,7 @@ %div{ class: container_class } .sub-header-block - Compare branches, tags or commit ranges. + Compare Git revisions. %br Fill input field with commit id like %code.label-branch 4eedf23 diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 7b7e37353e3..387ff018073 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,12 +1,18 @@ - if can?(current_user, :create_deployment, deployment) && deployment.deployable .pull-right + + - external_url = deployment.environment.external_url + - if external_url + = link_to external_url, target: '_blank', class: 'btn external-url' do + = icon('external-link') + - actions = deployment.manual_actions - if actions.present? .inline .dropdown %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play') - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| %li diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index fce7b1cb34e..ca0005abd0c 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -5,11 +5,13 @@ %td = render 'projects/deployments/commit', deployment: deployment - %td + %td.build-column - if deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do - = user_avatar(user: deployment.user, size: 20) + = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do = "#{deployment.deployable.name} (##{deployment.deployable.id})" + - if deployment.user + by + = user_avatar(user: deployment.user, size: 20) %td #{time_ago_with_tooltip(deployment.created_at)} diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 576e7ef021a..067cf595da3 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,4 +1,5 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) +- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files .content-block.oneline-block.files-changed @@ -20,7 +21,7 @@ - if diff_files.overflow? = render 'projects/diffs/warning', diff_files: diff_files -.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}} +.files{ data: { can_create_note: can_create_note } } - diff_files.each_with_index do |diff_file, index| - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index d07de45fdde..257e0a855bd 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -8,7 +8,7 @@ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - + = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option') - if editable_diff?(diff_file) - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a04d53e02bf..c8f84b96cb7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -91,7 +91,7 @@ Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control' + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } - if Gitlab.config.registry.enabled .form-group @@ -100,7 +100,8 @@ = f.check_box :container_registry_enabled %strong Container Registry %br - %span.descr Enable Container Registry for this repository + %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', f: f %hr diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 205392b1e81..19c536897bd 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -4,10 +4,17 @@ %td = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) - %td + %td.deployment-column - if last_deployment - = user_avatar(user: last_deployment.user, size: 20) - %strong ##{last_deployment.id} + %span ##{last_deployment.iid} + - if last_deployment.user + by + = user_avatar(user: last_deployment.user, size: 20) + + %td + - if last_deployment && last_deployment.deployable + = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do + = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})" %td - if last_deployment diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml index 6d1bdb9320f..3871165763c 100644 --- a/app/views/projects/environments/edit.html.haml +++ b/app/views/projects/environments/edit.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title "Edit", @environment.name, "Environments" += render "projects/pipelines/head" -%h3.page-title - Edit environment -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + Edit environment + %hr + = render 'form' diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 11e2ff506b4..f0b64a8775b 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -22,25 +22,27 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - - if @all_environments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any environments right now. - %p.blank-state-text - Environments are places where code gets deployed, such as staging or production. - %br - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - if can?(current_user, :create_environment, @project) - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do - New environment - - else - .table-holder - %table.table.builds.environments - %tbody - %th Environment - %th Last Deployment - %th Commit - %th - %th.hidden-xs - = render @environments + .environments-container + - if @environments.blank? + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any environments right now. + %p.blank-state-text + Environments are places where code gets deployed, such as staging or production. + %br + = succeed "." do + = link_to "Read more about environments", help_page_path("ci/environments") + - if can?(current_user, :create_environment, @project) + = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do + New environment + - else + .table-holder + %table.table.ci-table.environments + %tbody + %th Environment + %th Last Deployment + %th Build + %th Commit + %th + %th.hidden-xs + = render @environments diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index e51667ade2d..24638c77cbb 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title 'New Environment' += render "projects/pipelines/head" -%h3.page-title - New environment -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + New environment + %hr + = render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 4981b95a19b..c4209d499ad 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -15,26 +15,27 @@ - if @environment.opened? && last_deployment.try(:close_action) = link_to 'Close', [:play, @project.namespace.becomes(Namespace), @project, last_deployment.close_action], data: { confirm: 'Are you sure you want to close this environment?' }, class: 'btn btn-danger', method: :post - - if @deployments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any deployments right now. - %p.blank-state-text - Define environments in the deploy stage(s) in - %code .gitlab-ci.yml - to track deployments here. - = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - - else - .table-holder - %table.table.builds.environments - %thead - %tr - %th ID - %th Commit - %th Build - %th - %th.hidden-xs + .deployments-container + - if @deployments.blank? + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any deployments right now. + %p.blank-state-text + Define environments in the deploy stage(s) in + %code .gitlab-ci.yml + to track deployments here. + = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" + - else + .table-holder + %table.table.ci-table.environments + %thead + %tr + %th ID + %th Commit + %th Build + %th + %th.hidden-xs - = render @deployments + = render @deployments - = paginate @deployments, theme: 'gitlab' + = paginate @deployments, theme: 'gitlab' diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index bacc5708e4b..abf4f697f86 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -15,7 +15,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 331dc1fcc29..80fe6be49b0 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -62,5 +62,3 @@ %td.coverage - if generic_commit_status.try(:coverage) #{generic_commit_status.coverage}% - - %td diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 409f4701e4b..0a66d60accc 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,7 +1,9 @@ - if subject.target_url = link_to subject.target_url do - = render_status_with_link('commit status', subject.status) + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name - else - = render_status_with_link('commit status', subject.status) + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml index 4c5dd9b88bf..1b0dbbb8111 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/index.html.haml @@ -16,7 +16,7 @@ = label_tag :link_group_access, "Max access level", class: "label-light" .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" - %span.caret + = icon('caret-down') .form-group = label_tag :expires_at, 'Access expiration date', class: 'label-light' .clearable-input diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml new file mode 100644 index 00000000000..af9a5b19060 --- /dev/null +++ b/app/views/projects/group_links/update.js.haml @@ -0,0 +1,3 @@ +:plain + var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); + $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name')); diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 509b01c548a..4825820c4d9 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -10,7 +10,7 @@ Issues = nav_link(controller: :boards) do - = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do + = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do %span Board diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 7cf1923456e..3a6fbbc7fbc 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "Issues" %h3.page-title Edit Issue ##{@issue.iid} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 3fb4191c60e..09347ad5fff 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "#{@issue.to_reference} #{@issue.title}", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes @@ -23,8 +23,8 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - %span.caret Options + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can?(current_user, :create_issue, @project) @@ -55,12 +55,12 @@ .issue-details.issuable-details .detail-page-description.content-block %h2.title - = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author + = markdown_field(@issue, :title) - if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .wiki = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author) + = markdown_field(@issue, :description) %textarea.hidden.js-task-list-field = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 73c6f2a046c..71f7f354d72 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -5,7 +5,7 @@ .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } Options - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right %ul %li diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 6901ba13ab7..52b187e7e58 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title "Edit", @label.name, "Labels" += render "projects/issues/head" -%h3.page-title - Edit Label -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + Edit Label + %hr + = render 'form' diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 49ddf901619..a1bb66cfb6c 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title "New Label" += render "projects/issues/head" -%h3.page-title - New Label -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + New Label + %hr + = render 'form' diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index de39964fca8..466ec1475d8 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -65,19 +65,6 @@ - if @merge_request.errors.any? = form_errors(@merge_request) - - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? - .light-well.append-bottom-default - .center - %h4 - There isn't anything to merge. - %p.slead - - if @merge_request.source_branch == @merge_request.target_branch - You'll need to use different branch names to get a valid comparison. - - else - %span.label-branch #{@merge_request.source_branch} - and - %span.label-branch #{@merge_request.target_branch} - are the same. = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml new file mode 100644 index 00000000000..74367ab9b7b --- /dev/null +++ b/app/views/projects/merge_requests/_new_diffs.html.haml @@ -0,0 +1 @@ += render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 00bd4e143df..da6927879a4 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -18,34 +18,38 @@ = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.commits-tab - = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do - Commits - %span.badge= @commits.size - - if @pipeline - %li.builds-tab.active - = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size - %li.diffs-tab.active - = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @diffs.real_size + - if @commits.empty? + .commits-empty + %h4 + There are no commits yet. + = custom_icon ('illustration_no_commits') + - else + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.commits-tab.active + = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do + Commits + %span.badge= @commits.size + - if @pipeline + %li.builds-tab + = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do + Changes + %span.badge= @merge_request.diff_size - .tab-content - #commits.commits.tab-pane - = render "projects/merge_requests/show/commits" - #diffs.diffs.tab-pane.active - - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .alert.alert-danger - %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. - %p To preserve performance the line changes are not shown. - - else - = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false - - if @pipeline - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" + .tab-content + #commits.commits.tab-pane.active + = render "projects/merge_requests/show/commits" + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX + - if @pipeline + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" + + .mr-loading-status + = spinner :javascript $('.assign-to-me-link').on('click', function(e){ @@ -54,6 +58,6 @@ }); :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'diffs' : 'new')}", - setUrl: false + action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", + buildsLoaded: "#{@pipeline ? 'true' : 'false'}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 9f34ca9ff4e..662463bc72b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" +- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do @@ -22,7 +22,7 @@ %span.dropdown.inline.prepend-left-5 %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } Download as - %span.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) @@ -47,7 +47,7 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits_count.nonzero? - %ul.merge-request-tabs.nav-links.no-top.no-bottom + %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 03159f123f3..7c3ac6652ee 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" +- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" %h3.page-title Edit Merge Request #{@merge_request.to_reference} diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index ebf18f6ac85..ed23d06ee5e 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,13 +1,13 @@ .detail-page-description.content-block %h2.title - = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author + = markdown_field(@merge_request, :title) %div - if @merge_request.description.present? .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author) + = markdown_field(@merge_request, :description) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index e35291dff7d..e7c5bca6a37 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,8 +19,8 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - %span.caret Options + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul %li{ class: merge_request_button_visibility(@merge_request, true) } diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 904452fcc4f..eab48b78cb3 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -9,7 +9,7 @@ latest version - else version #{version_index(@merge_request_diff)} - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Version: @@ -39,7 +39,7 @@ version #{version_index(@start_version)} - else #{@merge_request.target_branch} - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Compared with: @@ -64,6 +64,16 @@ #{@merge_request.target_branch} (base) .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + - if different_base?(@start_version, @merge_request_diff) + .content-block + = icon('info-circle') + Selected versions have different base commits. + Changes will include + = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do + new commits + from + %code #{@merge_request.target_branch} + - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block = icon('info-circle') diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index a92f0f9f481..a82c846baa7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -44,20 +44,5 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. -- @merge_request.environments.sort_by(&:name).each do |environment| - - if can?(current_user, :read_environment, environment) - .mr-widget-heading - .ci_widget.ci-success - = ci_icon_for_status("success") - %span.hidden-sm - Deployed to - = succeed '.' do - = link_to environment.name, environment_path(environment), class: 'environment' - - external_url = environment.external_url - - if external_url - = link_to external_url, target: '_blank' do - = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) - %span.close-env-container - - if environment.closeable? - = link_to [:play, @project.namespace.becomes(Namespace), @project, environment.close_action], method: :post, class: 'close-evn-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to close this environment?' } do - = icon('stop-circle-o', text: 'Stop environment') +.js-success-icon.hidden + = ci_icon_for_status('success') diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 6f5ee5f16c5..842b6df310d 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -35,3 +35,4 @@ Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} = succeed '.' do != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author + = mr_assign_issues_link diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index ea618263a4a..608fdf1c5f5 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -12,6 +12,7 @@ merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", ci_message: { @@ -33,4 +34,4 @@ merge_request_widget.clearEventListeners(); } - merge_request_widget = new MergeRequestWidget(opts); + merge_request_widget = new window.gl.MergeRequestWidget(opts); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index bf2e76f0083..ce43ca3a286 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -12,7 +12,7 @@ Merge When Build Succeeds - unless @project.only_allow_merge_if_build_succeeds? = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do - %span.caret + = icon('caret-down') %span.sr-only Select Merge Moment %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index be682226ab6..11f41e75e63 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,8 +1,12 @@ +- @no_container = true - page_title "Edit", @milestone.title, "Milestones" += render "projects/issues/head" -%h3.page-title - Edit Milestone ##{@milestone.iid} +%div{ class: container_class } -%hr + %h3.page-title + Edit Milestone ##{@milestone.iid} -= render "form" + %hr + + = render "form" diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 7f372b41698..cda093ade81 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,8 +1,11 @@ +- @no_container = true - page_title "New Milestone" += render "projects/issues/head" -%h3.page-title - New Milestone +%div{ class: container_class } + %h3.page-title + New Milestone -%hr + %hr -= render "form" + = render "form" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 73772cc0e32..c83818e9199 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,49 +1,52 @@ +- @no_container = true - page_title @milestone.title, "Milestones" - page_description @milestone.description += render "projects/issues/head" -.detail-page-header - .status-box{ class: status_box_class(@milestone) } - - if @milestone.closed? - Closed - - elsif @milestone.expired? - Past due - - else - Open - %span.identifier - Milestone ##{@milestone.iid} - - if @milestone.expires_at - %span.creator - · - = @milestone.expires_at - .pull-right - - if can?(current_user, :admin_milestone, @project) - - if @milestone.active? - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" +%div{ class: container_class } + .detail-page-header + .status-box{ class: status_box_class(@milestone) } + - if @milestone.closed? + Closed + - elsif @milestone.expired? + Past due - else - = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" + Open + %span.identifier + Milestone ##{@milestone.iid} + - if @milestone.expires_at + %span.creator + · + = @milestone.expires_at + .pull-right + - if can?(current_user, :admin_milestone, @project) + - if @milestone.active? + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" + - else + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do - Edit + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do + Edit - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do - Delete + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do + Delete -.detail-page-description.milestone-detail - %h2.title - = markdown escape_once(@milestone.title), pipeline: :single_line - %div - - if @milestone.description.present? - .description - .wiki - = preserve do - = markdown @milestone.description + .detail-page-description.milestone-detail + %h2.title + = markdown_field(@milestone, :title) + %div + - if @milestone.description.present? + .description + .wiki + = preserve do + = markdown_field(@milestone, :description) -- if @milestone.total_items_count(current_user).zero? - .alert.alert-success.prepend-top-default - %span Assign some issues to this milestone. -- elsif @milestone.complete?(current_user) && @milestone.active? - .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. You may close this milestone now. + - if @milestone.total_items_count(current_user).zero? + .alert.alert-success.prepend-top-default + %span Assign some issues to this milestone. + - elsif @milestone.complete?(current_user) && @milestone.active? + .alert.alert-success.prepend-top-default + %span All issues for this milestone are closed. You may close this milestone now. -= render 'shared/milestones/summary', milestone: @milestone, project: @project -= render 'shared/milestones/tabs', milestone: @milestone + = render 'shared/milestones/summary', milestone: @milestone, project: @project + = render 'shared/milestones/tabs', milestone: @milestone diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index b2ece44d966..29df1bab04e 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -8,7 +8,7 @@ .project-network .controls = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Git revision", class: 'search-input form-control input-mx-250 search-sha' = button_tag class: 'btn btn-success' do = icon('search') .inline.prepend-left-20 diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 788be4a0047..73fe6a715fa 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -61,7 +61,7 @@ .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text.md = preserve do - = note.note_html + = note.redacted_note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 8352eba7446..00b62a595ff 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -14,7 +14,7 @@ .disabled-comment.text-center .disabled-comment-text.inline Please - = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes') + = link_to "register", new_session_path(:user, redirect_to_referer: 'yes') or = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes') to post a comment diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 7d421c0e740..b10dd47709f 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,7 +1,7 @@ = content_for :sub_nav do .scrolling-tabs-container.sub-nav-scroll = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs + .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } %ul{ class: (container_class) } - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 5800ef7de48..d288efc546f 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -33,7 +33,7 @@ - if @commit .commit-box.content-block %h3.commit-title - = markdown escape_once(@commit.title), pipeline: :single_line + = markdown(@commit.title, pipeline: :single_line) - if @commit.description.present? %pre.commit-description - = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) + = preserve(markdown(@commit.description, pipeline: :single_line)) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 2d1df095bfa..9eeef5f57b4 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -43,7 +43,7 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.builds + %table.table.ci-table %thead %th.col-xs-1.col-sm-1 Status %th.col-xs-2.col-sm-4 Pipeline diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 75943c64276..688535ad764 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -1,8 +1,11 @@ +- @no_container = true - page_title "Pipeline" += render "projects/pipelines/head" -.prepend-top-default - - if @commit - = render "projects/pipelines/info" - %div.block-connector +%div{ class: container_class } + .prepend-top-default + - if @commit + = render "projects/pipelines/info" + %div.block-connector -= render "projects/commit/pipeline", pipeline: @pipeline + = render "projects/commit/pipeline", pipeline: @pipeline diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index e783d8c72c5..9738f369a35 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -1,7 +1,7 @@ .panel.panel-default .panel-heading + Group members with access to %strong #{@group.name} - group members %span.badge= members.size - if can?(current_user, :admin_group_member, @group) .controls diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml new file mode 100644 index 00000000000..d7f5fa96527 --- /dev/null +++ b/app/views/projects/project_members/_groups.html.haml @@ -0,0 +1,7 @@ +.panel.panel-default.project-members-groups + .panel-heading + Groups with access to + %strong #{@project.name} + %span.badge= group_links.size + %ul.content-list + = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index fa8cbf71733..79dcd7a6ee9 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,27 +1,22 @@ -= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10 - = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) - .help-block += form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| + .row + .col-md-4.col-lg-6 + = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true) + .help-block.append-bottom-10 Search for users by name, username, or email, or invite new ones using their email address. - .form-group - = f.label :access_level, "Project Access", class: 'control-label' - .col-sm-10 - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2" - .help-block - Read more about role permissions - %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .col-md-3.col-lg-2 + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions - .form-group - = f.label :expires_at, 'Access expiration date', class: 'control-label' - .col-sm-10 + .col-md-3.col-lg-2 .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - .help-block + .help-block.append-bottom-10 On this date, the user(s) will automatically lose access to this project. - .form-actions - = f.submit 'Add users to project', class: "btn btn-create" + .col-md-2 + = f.submit "Add to project", class: "btn btn-create btn-block" diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index b0bfdd235f7..c1e894d8f40 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,19 +1,7 @@ .panel.panel-default .panel-heading + Users with access to %strong #{@project.name} - project members - %span.badge= members.size - .controls - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } - = button_tag class: 'btn', title: 'Search' do - = icon("search") + %span.badge= @project_members.total_count %ul.content-list = render partial: 'shared/members/member', collection: members, as: :member - -:javascript - $('form.member-search-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '?' + $(this).serialize()); - }); diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9d063b3081f..bdeb704b6da 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,24 +1,28 @@ - page_title "Members" -.project-members-page.js-project-members-page.prepend-top-default +.project-members-page.prepend-top-default + %h4.project-members-title.clearfix + Members + = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project" - if can?(current_user, :admin_project_member, @project) - .panel.panel-default - .panel-heading - Add new user to project - .controls - = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do - Import members - .panel-body - %p.light - Users with access to this project are listed below. - = render "new_project_member" + .project-members-new.append-bottom-default + %p.clearfix + Add new user to + %strong= @project.name + = render "new_project_member" - = render 'shared/members/requests', membership_source: @project, requesters: @requesters + = render 'shared/members/requests', membership_source: @project, requesters: @requesters - = render 'team', members: @project_members - - - if @group - = render "group_members", members: @group_members + .append-bottom-default.clearfix + %h5.member.existing-title + Existing users and groups + = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + - if @group_links.any? + = render 'groups', group_links: @group_links - - if @project_group_links.any? && @project.allowed_to_share_with_group? - = render "shared_group_members" + = render 'team', members: @project_members + = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 37e55dc72a3..91927181efb 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,3 @@ :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); - new gl.MemberExpirationDate(); + var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); + $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index 43a6fdfd103..d9c39fb87b7 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author + = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author) %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 752b9e060d5..5afa193357e 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,8 +1,8 @@ %h3 Shared Runners .bs-callout.bs-callout-warning.shared-runners-description - - if shared_runners_text.present? - = markdown(shared_runners_text, pipeline: 'plain_markdown') + - if current_application_settings.shared_runners_text.present? + = markdown_field(current_application_settings, :shared_runners_text) - else GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 9773b8438ec..32e1f8a21b0 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -12,7 +12,7 @@ .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } Options - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-menu-full-width %ul - if can?(current_user, :create_project_snippet, @project) diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index a156d98bab8..05fccb4f976 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -30,4 +30,4 @@ .description.prepend-top-default .wiki = preserve do - = markdown release.description + = markdown_field(release, :description) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 6adbe9351dc..7a0d9dcc94f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -14,7 +14,7 @@ %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } %span.light = @sort.humanize - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_tags_path(sort: nil) do diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 4dd7439b2d0..155af755759 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -33,6 +33,6 @@ .description .wiki = preserve do - = markdown @release.description + = markdown_field(@release, :description) - else This tag has no release notes. diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 8f68d6d1b87..e010f21de5a 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -7,7 +7,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author })) + = search_md_sanitize(issue, :description) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 6331c2bd6b0..07b17bc69c0 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -6,7 +6,7 @@ - if merge_request.description.present? .description.term = preserve do - = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author })) + = search_md_sanitize(merge_request, :description) %span.light #{merge_request.project.name_with_namespace} .pull-right diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml index b31595d8d1c..9664f65a36e 100644 --- a/app/views/search/results/_milestone.html.haml +++ b/app/views/search/results/_milestone.html.haml @@ -6,4 +6,4 @@ - if milestone.description.present? .description.term = preserve do - = search_md_sanitize(markdown(milestone.description)) + = search_md_sanitize(milestone, :description) diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index e0400083870..f3701b89bb4 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -23,4 +23,4 @@ .note-search-result .term = preserve do - = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author})) + = search_md_sanitize(note, :note) diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 8824bcc158e..3480800369a 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,4 +1,5 @@ %ul.nav-links.event-filter.scrolling-tabs + = event_filter_link EventFilter.all, 'All' = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 77676454b57..6f593e8dff9 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -12,4 +12,4 @@ = link_to_label(label, tooltip: false) - if label.description %span.label-description - = markdown(label.description, pipeline: :single_line) + = markdown_field(label, :description) diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index 51622931e24..fbbf6f358c5 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -3,7 +3,7 @@ = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' } %a.btn.btn-new.new-project-item-select-button = local_assigns[:label] - %b.caret + = icon('caret-down') :javascript $('.new-project-item-select-button').on('click', function() { diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 36bbac6fbf5..68e05cb72e1 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -5,7 +5,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to page_filter_path(sort: sort_value_priority, label: true) do diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 1ad95351005..dc4ee3074d2 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -35,4 +35,4 @@ - if group.description.present? .description - = markdown(group.description, pipeline: :description) + = markdown_field(group, :description) diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg new file mode 100644 index 00000000000..4f9d9add60d --- /dev/null +++ b/app/views/shared/icons/_illustration_no_commits.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c3f4e10c954..a7944a60130 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -23,6 +23,8 @@ data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do %ul.dropdown-footer-list %li + %a.no-template + No template %a.reset-template Reset template %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } @@ -85,20 +87,20 @@ .issuable-form-select-holder - if issuable.assignee_id = f.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee", show_menu_above: true } }) .form-group.issue-milestone = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input" + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" = f.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' } + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" - if has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 6d307611640..22b5a6aa11b 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -8,6 +8,7 @@ - classes = local_assigns.fetch(:classes, []) - selected = local_assigns.fetch(:selected, nil) - selected_toggle = local_assigns.fetch(:selected_toggle, nil) +- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) - classes << 'js-extra-options' if extra_options @@ -23,7 +24,7 @@ = multi_label_name(selected, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } + = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index ab3cc33d18f..f27a9002ec2 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -2,9 +2,10 @@ - extra_class = extra_class || '' - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) +- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) -= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", += dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project %ul.dropdown-footer-list diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml new file mode 100644 index 00000000000..1c0346bbc78 --- /dev/null +++ b/app/views/shared/members/_group.html.haml @@ -0,0 +1,29 @@ +- group_link = local_assigns[:group_link] +- group = group_link.group +- can_admin_member = can?(current_user, :admin_project_member, @project) +%li.member.group_member{ id: "group_member_#{group_link.id}" } + %span{ class: "list-item-name" } + = image_tag group_icon(group), class: "avatar s40", alt: '' + %strong + = link_to group.name, group_path(group) + .cgray + Joined #{time_ago_with_tooltip(group.created_at)} + - if group_link.expires? + · + %span{ class: ('text-warning' if group_link.expires_soon?) } + Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} + .controls.member-controls + = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do + = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member + .prepend-left-5.clearable-input.member-form-control + = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member + %i.clear-icon.js-clear-input + - if can_admin_member + = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), + remote: true, + method: :delete, + data: { confirm: "Are you sure you want to remove #{group.name}?" }, + class: 'btn btn-remove prepend-left-10' do + %span.visible-xs-block + Delete + = icon('trash', class: 'hidden-xs') diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 5f20e4bd42a..432047a1c4e 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,59 +1,29 @@ - show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) -- user = member.user +- user = local_assigns.fetch(:user, member.user) +- source = member.source +- can_admin_member = can?(current_user, action_member_permission(:update, member), member) -%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) } - - if show_roles - .controls - %strong.control-text= member.human_access - - if show_controls - - if !user && can?(current_user, action_member_permission(:admin, member), member.source) - = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), - method: :post, - class: 'btn' - - - if can?(current_user, action_member_permission(:update, member), member) - = button_tag icon('pencil'), - type: 'button', - class: 'btn inline js-toggle-button', - title: 'Edit' - - - if member.request? - = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), - method: :post, - class: 'btn btn-success', - title: 'Grant access' - - - if can?(current_user, action_member_permission(:destroy, member), member) - - if current_user == user - = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), - method: :delete, - data: { confirm: leave_confirmation_message(member.source) }, - class: 'btn btn-remove' - - else - = link_to icon('trash'), member, - remote: true, - method: :delete, - data: { confirm: remove_member_message(member) }, - class: 'btn btn-remove', - title: remove_member_title(member) - - - %span{ class: ("list-item-name" if show_controls) } +%li.member{ class: dom_class(member), id: dom_id(member) } + %span.list-item-name - if user = image_tag avatar_icon(user, 40), class: "avatar s40", alt: '' %strong = link_to user.name, user_path(user) - %span.cgray= user.username + %span.cgray= user.to_reference - if user == current_user - %span.label.label-success It's you + %span.label.label-success.prepend-left-5 It's you - if user.blocked? %label.label.label-danger %strong Blocked - .cgray + - if source.instance_of?(Group) && !@group + = link_to source, class: "member-group-link prepend-left-5" do + = "· #{source.name}" + + .hidden-xs.cgray - if member.request? Requested = time_ago_with_tooltip(member.requested_at) @@ -73,20 +43,44 @@ by = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_roles - .edit-member.hide.js-toggle-content - %br - = form_for member, remote: true, html: { class: 'form-horizontal' } do |f| - .form-group - = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label' - .col-sm-10 - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}" - .form-group - = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label' - .col-sm-10 - .clearable-input - = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}" + .controls.member-controls + - if show_controls + - if user != current_user + = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| + = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member + .prepend-left-5.clearable-input.member-form-control + = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input - .prepend-top-10 - = f.submit 'Save', class: 'btn btn-save btn-sm' + - else + %span.member-access-text= member.human_access + + - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) + = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), + method: :post, + class: 'btn btn-default prepend-left-10' + + - elsif member.request? && can_admin_member + = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), + method: :post, + class: 'btn btn-success prepend-left-10', + title: 'Grant access' + + - if can?(current_user, action_member_permission(:destroy, member), member) + - if current_user == user + = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(member.source) }, + class: 'btn btn-remove prepend-left-10' + - else + = link_to member, + remote: true, + method: :delete, + data: { confirm: remove_member_message(member) }, + class: 'btn btn-remove prepend-left-10', + title: remove_member_title(member) do + %span.visible-xs-block + Delete + = icon('trash', class: 'hidden-xs') + - else + %span.member-access-text= member.human_access diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 40b39e850b0..10050adfda5 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -1,8 +1,8 @@ - if requesters.any? .panel.panel-default .panel-heading + Users requesting access to %strong= membership_source.name - access requests %span.badge= requesters.size %ul.content-list = render partial: 'shared/members/member', collection: requesters, as: :member diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index b15e8ea73fe..33f93dccd3c 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -8,7 +8,7 @@ = link_to milestones_label_path(options) do - render_colored_label(label, tooltip: false) %span.prepend-description-left - = markdown(label.description, pipeline: :single_line) + = markdown_field(label, :description) .pull-info-right %span.append-right-20 diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 7ff947a51db..548215243db 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -26,7 +26,7 @@ .detail-page-description.milestone-detail %h2.title - = markdown escape_once(milestone.title), pipeline: :single_line + = markdown_field(milestone, :title) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default @@ -55,4 +55,3 @@ Open %td = ms.expires_at - diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index ff1cf966a9b..feaa5570c21 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -11,7 +11,7 @@ = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } - %span.caret + = icon('caret-down') .sr-only Toggle dropdown - else %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 66c309644a7..e8668048703 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -50,4 +50,4 @@ class: "commit-row-message" - elsif project.description.present? .description - = markdown(project.description, pipeline: :description) + = markdown_field(project, :description) diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 773ce8ac240..dcdba01aee9 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,9 +1,12 @@ - unless @snippet.content.empty? - if markup?(@snippet.file_name) %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}} - = @snippet.data + = @snippet.content .file-content.wiki - = render_markup(@snippet.file_name, @snippet.data) + - if gitlab_markdown?(@snippet.file_name) + = preserve(markdown_field(@snippet, :content)) + - else + = render_markup(@snippet.file_name, @snippet.content) - else = render 'shared/file_highlight', blob: @snippet - else diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 7ae4211ddfd..d7506e07ff6 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -21,4 +21,4 @@ = render "snippets/actions" %h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author + = markdown_field(@snippet, :title) diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index c446dc3bdc1..1d0e549ed3d 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -12,7 +12,7 @@ .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } Options - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-menu-full-width %ul %li diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index cd89155c616..27d7a6c5bb6 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -9,6 +9,7 @@ .file-actions = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" = render 'shared/snippets/blob' = render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 9657101ace5..232ca26c1af 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -6,7 +6,7 @@ %script#js-authenticate-u2f-setup{ type: "text/template" } %div %p Insert your security key (if you haven't already), and press the button below. - %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device + %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device %script#js-authenticate-u2f-in-progress{ type: "text/template" } %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 60fc0c0daf6..1e0752bd3c3 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -40,11 +40,11 @@ .user-info .cover-title = @user.name - %span.handle - @#{@user.username} .cover-desc.member-date %span.middle-dot-divider + @#{@user.username} + %span.middle-dot-divider Member since #{@user.created_at.to_s(:medium)} .cover-desc @@ -82,7 +82,7 @@ %ul.nav-links.center.user-profile-nav %li.js-activity-tab - = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity %li.js-groups-tab = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb new file mode 100644 index 00000000000..0680645a8db --- /dev/null +++ b/app/workers/build_coverage_worker.rb @@ -0,0 +1,9 @@ +class BuildCoverageWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(build_id) + Ci::Build.find_by(id: build_id) + .try(:update_coverage) + end +end diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb new file mode 100644 index 00000000000..e7286b77ac5 --- /dev/null +++ b/app/workers/build_finished_worker.rb @@ -0,0 +1,10 @@ +class BuildFinishedWorker + include Sidekiq::Worker + + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + BuildCoverageWorker.new.perform(build.id) + BuildHooksWorker.new.perform(build.id) + end + end +end diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb new file mode 100644 index 00000000000..e22ececb3fd --- /dev/null +++ b/app/workers/build_hooks_worker.rb @@ -0,0 +1,9 @@ +class BuildHooksWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(build_id) + Ci::Build.find_by(id: build_id) + .try(:execute_hooks) + end +end diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb new file mode 100644 index 00000000000..500d357ce31 --- /dev/null +++ b/app/workers/build_success_worker.rb @@ -0,0 +1,27 @@ +class BuildSuccessWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + create_deployment(build) + end + end + + private + + def create_deployment(build) + return if build.environment.blank? + + service = CreateDeploymentService.new( + build.project, build.user, + environment: build.environment, + sha: build.sha, + ref: build.ref, + tag: build.tag, + options: build.options.to_h[:environment], + variables: build.variables) + + service.execute(build) + end +end diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb new file mode 100644 index 00000000000..c541daba50e --- /dev/null +++ b/app/workers/clear_database_cache_worker.rb @@ -0,0 +1,23 @@ +# This worker clears all cache fields in the database, working in batches. +class ClearDatabaseCacheWorker + include Sidekiq::Worker + + BATCH_SIZE = 1000 + + def perform + CacheMarkdownField.caching_classes.each do |kls| + fields = kls.cached_markdown_fields.html_fields + clear_cache_fields = fields.each_with_object({}) do |field, memo| + memo[field] = nil + end + + Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}") + + kls.unscoped.in_batches(of: BATCH_SIZE) do |relation| + relation.update_all(clear_cache_fields) + end + end + + nil + end +end diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index c64ea108d52..174eabff9fd 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -2,12 +2,11 @@ class ExpireBuildArtifactsWorker include Sidekiq::Worker def perform - Rails.logger.info 'Cleaning old build artifacts' + Rails.logger.info 'Scheduling removal of build artifacts' - builds = Ci::Build.with_expired_artifacts - builds.find_each(batch_size: 50).each do |build| - Rails.logger.debug "Removing artifacts build #{build.id}..." - build.erase_artifacts! - end + build_ids = Ci::Build.with_expired_artifacts.pluck(:id) + build_ids = build_ids.map { |build_id| [build_id] } + + Sidekiq::Client.push_bulk('class' => ExpireBuildInstanceArtifactsWorker, 'args' => build_ids ) end end diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb new file mode 100644 index 00000000000..916c2e633c1 --- /dev/null +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -0,0 +1,11 @@ +class ExpireBuildInstanceArtifactsWorker + include Sidekiq::Worker + + def perform(build_id) + build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id) + return unless build + + Rails.logger.info "Removing artifacts build #{build.id}..." + build.erase_artifacts! + end +end diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb new file mode 100644 index 00000000000..ab5e9f6daad --- /dev/null +++ b/app/workers/pipeline_hooks_worker.rb @@ -0,0 +1,9 @@ +class PipelineHooksWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id) + .try(:execute_hooks) + end +end diff --git a/app/workers/process_pipeline_worker.rb b/app/workers/pipeline_process_worker.rb index 26ea5f1c24d..f44227d7086 100644 --- a/app/workers/process_pipeline_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -1,4 +1,4 @@ -class ProcessPipelineWorker +class PipelineProcessWorker include Sidekiq::Worker sidekiq_options queue: :default diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb new file mode 100644 index 00000000000..5dd443fea59 --- /dev/null +++ b/app/workers/pipeline_success_worker.rb @@ -0,0 +1,12 @@ +class PipelineSuccessWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| + MergeRequests::MergeWhenBuildSucceedsService + .new(pipeline.project, nil) + .trigger(pipeline) + end + end +end diff --git a/app/workers/update_pipeline_worker.rb b/app/workers/pipeline_update_worker.rb index 6ef5678073e..44a7f24e401 100644 --- a/app/workers/update_pipeline_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -1,4 +1,4 @@ -class UpdatePipelineWorker +class PipelineUpdateWorker include Sidekiq::Worker sidekiq_options queue: :default diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb new file mode 100644 index 00000000000..df4c4a6628b --- /dev/null +++ b/app/workers/trending_projects_worker.rb @@ -0,0 +1,11 @@ +class TrendingProjectsWorker + include Sidekiq::Worker + + sidekiq_options queue: :trending_projects + + def perform + Rails.logger.info('Refreshing trending projects') + + TrendingProject.refresh! + end +end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb new file mode 100644 index 00000000000..03f0528cdae --- /dev/null +++ b/app/workers/update_merge_requests_worker.rb @@ -0,0 +1,16 @@ +class UpdateMergeRequestsWorker + include Sidekiq::Worker + + def perform(project_id, user_id, oldrev, newrev, ref) + project = Project.find_by(id: project_id) + return unless project + + user = User.find_by(id: user_id) + return unless user + + MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) + + push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, []) + SystemHooksService.new.execute_hooks(push_data, :push_hooks) + end +end |