diff options
Diffstat (limited to 'app')
159 files changed, 1774 insertions, 810 deletions
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js index 5e3c45f7e92..20ab2d7e827 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -1,5 +1,3 @@ -import spreadString from './spread_string'; - // On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ const flagACodePoint = 127462; // parseInt('1F1E6', 16) const flagZCodePoint = 127487; // parseInt('1F1FF', 16) @@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) { const tone1 = 127995;// parseInt('1F3FB', 16) const tone5 = 127999;// parseInt('1F3FF', 16) function isSkinToneComboEmoji(emojiUnicode) { - return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { + return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => { const cp = char.codePointAt(0); return cp >= tone1 && cp <= tone5; }); @@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) { // doesn't support the skin tone versions of horse racing const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) function isHorceRacingSkinToneComboEmoji(emojiUnicode) { - return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && isSkinToneComboEmoji(emojiUnicode); } @@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) function isPersonZwjEmoji(emojiUnicode) { let hasPersonEmoji = false; let hasZwj = false; - spreadString(emojiUnicode).forEach((character) => { + Array.from(emojiUnicode).forEach((character) => { const cp = character.codePointAt(0); if (cp === zwj) { hasZwj = true; diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js deleted file mode 100644 index 327764ec6e9..00000000000 --- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js +++ /dev/null @@ -1,50 +0,0 @@ -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known -function knownCharCodeAt(givenString, index) { - const str = `${givenString}`; - const end = str.length; - - const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; - let idx = index; - while ((surrogatePairs.exec(str)) != null) { - const li = surrogatePairs.lastIndex; - if (li - 2 < idx) { - idx += 1; - } else { - break; - } - } - - if (idx >= end || idx < 0) { - return NaN; - } - - const code = str.charCodeAt(idx); - - let high; - let low; - if (code >= 0xD800 && code <= 0xDBFF) { - high = code; - low = str.charCodeAt(idx + 1); - // Go one further, since one of the "characters" is part of a surrogate pair - return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - } - return code; -} - -// See http://stackoverflow.com/a/38901550/796832 -// ES5/PhantomJS compatible version of spreading a string -// -// [...'foo'] -> ['f', 'o', 'o'] -// [...'๐๐ฟ'] -> ['๐', '๐ฟ'] -function spreadString(str) { - const arr = []; - let i = 0; - while (!isNaN(knownCharCodeAt(str, i))) { - const codePoint = knownCharCodeAt(str, i); - arr.push(String.fromCodePoint(codePoint)); - i += 1; - } - return arr; -} - -export default spreadString; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 86927314dd4..576b8a0425f 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -18,7 +18,7 @@ // Button does not change visibility. If button has icon - it changes chevron style. // // %div.js-toggle-container - // %a.js-toggle-button + // %button.js-toggle-button // %div.js-toggle-content // $('body').on('click', '.js-toggle-button', function(e) { diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js new file mode 100644 index 00000000000..9b8bfbfc8c0 --- /dev/null +++ b/app/assets/javascripts/blob/notebook/index.js @@ -0,0 +1,85 @@ +/* eslint-disable no-new */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import NotebookLab from 'vendor/notebooklab'; + +Vue.use(VueResource); +Vue.use(NotebookLab); + +export default () => { + const el = document.getElementById('js-notebook-viewer'); + + new Vue({ + el, + data() { + return { + error: false, + loadError: false, + loading: true, + json: {}, + }; + }, + template: ` + <div class="container-fluid md prepend-top-default append-bottom-default"> + <div + class="text-center loading" + v-if="loading && !error"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="iPython notebook loading"> + </i> + </div> + <notebook-lab + v-if="!loading && !error" + :notebook="json" + code-css-class="code white" /> + <p + class="text-center" + v-if="error"> + <span v-if="loadError"> + An error occured whilst loading the file. Please try again later. + </span> + <span v-else> + An error occured whilst parsing the file. + </span> + </p> + </div> + `, + methods: { + loadFile() { + this.$http.get(el.dataset.endpoint) + .then((res) => { + this.json = res.json(); + this.loading = false; + }) + .catch((e) => { + if (e.status) { + this.loadError = true; + } + + this.error = true; + }); + }, + }, + mounted() { + if (gon.katex_css_url) { + const katexStyles = document.createElement('link'); + katexStyles.setAttribute('rel', 'stylesheet'); + katexStyles.setAttribute('href', gon.katex_css_url); + document.head.appendChild(katexStyles); + } + + if (gon.katex_js_url) { + const katexScript = document.createElement('script'); + katexScript.addEventListener('load', () => { + this.loadFile(); + }); + katexScript.setAttribute('src', gon.katex_js_url); + document.head.appendChild(katexScript); + } else { + this.loadFile(); + } + }, + }); +}; diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js new file mode 100644 index 00000000000..b7a0a195a92 --- /dev/null +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -0,0 +1,3 @@ +import renderNotebook from './notebook'; + +document.addEventListener('DOMContentLoaded', renderNotebook); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 149bfbc8e8b..e057ac8df02 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -79,7 +79,7 @@ $(() => { resp.json().forEach((board) => { const list = Store.addList(board); - if (list.type === 'done') { + if (list.type === 'closed') { list.position = Infinity; } }); diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index 9320848bcca..f591134c548 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -50,9 +50,7 @@ export default { this.showDetail = false; }, showIssue(e) { - const targetTagName = e.target.tagName.toLowerCase(); - - if (targetTagName === 'a' || targetTagName === 'button') return; + if (e.target.classList.contains('js-no-trigger')) return; if (this.showDetail) { this.showDetail = false; diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index ba44dc5ed94..a4629b092bf 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -84,20 +84,20 @@ import eventHub from '../eventhub'; #{{ issue.id }} </span> <a - class="card-assignee has-tooltip" + class="card-assignee has-tooltip js-no-trigger" :href="rootPath + issue.assignee.username" :title="'Assigned to ' + issue.assignee.name" v-if="issue.assignee" data-container="body"> <img - class="avatar avatar-inline s20" + class="avatar avatar-inline s20 js-no-trigger" :src="issue.assignee.avatar" width="20" height="20" :alt="'Avatar for ' + issue.assignee.name" /> </a> <button - class="label color-label has-tooltip" + class="label color-label has-tooltip js-no-trigger" v-for="label in issue.labels" type="button" v-if="showLabel(label)" diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index d8322b34d44..772ea4c5565 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -48,7 +48,7 @@ import Vue from 'vue'; template: ` <div class="block list" - v-if="list.type !== 'done'"> + v-if="list.type !== 'closed'"> <button class="btn btn-default btn-block" type="button" diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index f18ad2a0fac..91e5fb2a666 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -10,7 +10,7 @@ class List { this.position = obj.position; this.title = obj.title; this.type = obj.list_type; - this.preset = ['done', 'blank'].indexOf(this.type) > -1; + this.preset = ['closed', 'blank'].indexOf(this.type) > -1; this.page = 1; this.loading = true; this.loadingMore = false; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 8912f234aa6..bcda70d0638 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -45,7 +45,7 @@ import Cookies from 'js-cookie'; }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'done')[0]); + return !(this.state.lists.filter(list => list.type !== 'closed')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -98,7 +98,7 @@ import Cookies from 'js-cookie'; issueTo.removeLabel(listFrom.label); } - if (listTo.type === 'done') { + if (listTo.type === 'closed') { issueLists.forEach((list) => { list.removeIssue(issue); }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index a20e5bc3b1b..4d5a857d705 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -33,12 +33,11 @@ export default Vue.component('pipelines-table', { * @return {Object} */ data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const store = new PipelineStore(); return { - endpoint: pipelinesTableData.endpoint, - helpPagePath: pipelinesTableData.helpPagePath, + endpoint: null, + helpPagePath: null, store, state: store.state, isLoading: false, @@ -65,6 +64,8 @@ export default Vue.component('pipelines-table', { * */ beforeMount() { + this.endpoint = this.$el.dataset.endpoint; + this.helpPagePath = this.$el.dataset.helpPagePath; this.service = new PipelinesService(this.endpoint); this.fetchPipelines(); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d1a662459e1..80490052389 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -33,6 +33,8 @@ /* global ProjectShow */ /* global Labels */ /* global Shortcuts */ +/* global Sidebar */ + import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; @@ -118,6 +120,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'groups:milestones:show': case 'dashboard:milestones:show': new Milestone(); + new Sidebar(); break; case 'dashboard:todos:index': new gl.Todos(); diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js index 455a8819549..385085c03e2 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js +++ b/app/assets/javascripts/environments/components/environment_actions.js @@ -25,6 +25,12 @@ export default { }; }, + computed: { + title() { + return 'Deploy to...'; + }, + }, + methods: { onClickAction(endpoint) { this.isLoading = true; @@ -44,8 +50,11 @@ export default { template: ` <div class="btn-group" role="group"> <button - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" + class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" + data-container="body" data-toggle="dropdown" + :title="title" + :aria-label="title" :disabled="isLoading"> <span> <span v-html="playIconSvg"></span> diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js index b4f9eb357fd..d79b916c360 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -9,13 +9,21 @@ export default { }, }, + computed: { + title() { + return 'Open'; + }, + }, + template: ` <a - class="btn external_url" + class="btn external-url has-tooltip" + data-container="body" :href="externalUrl" target="_blank" - rel="noopener noreferrer" - title="Environment external URL"> + rel="noopener noreferrer nofollow" + :title="title" + :aria-label="title"> <i class="fa fa-external-link" aria-hidden="true"></i> </a> `, diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js index 66ed10e19d1..9c196562c6c 100644 --- a/app/assets/javascripts/environments/components/environment_item.js +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -5,6 +5,7 @@ import ExternalUrlComponent from './environment_external_url'; import StopComponent from './environment_stop'; import RollbackComponent from './environment_rollback'; import TerminalButtonComponent from './environment_terminal_button'; +import MonitoringButtonComponent from './environment_monitoring'; import CommitComponent from '../../vue_shared/components/commit'; /** @@ -22,6 +23,7 @@ export default { 'stop-component': StopComponent, 'rollback-component': RollbackComponent, 'terminal-button-component': TerminalButtonComponent, + 'monitoring-button-component': MonitoringButtonComponent, }, props: { @@ -392,6 +394,14 @@ export default { return ''; }, + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + /** * Constructs folder URL based on the current location and the folder id. * @@ -496,13 +506,16 @@ export default { <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL"/> - <stop-component v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path" - :service="service"/> + <monitoring-button-component v-if="monitoringUrl && canReadEnvironment" + :monitoring-url="monitoringUrl"/> <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path"/> + <stop-component v-if="hasStopAction && canCreateDeployment" + :stop-url="model.stop_path" + :service="service"/> + <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js new file mode 100644 index 00000000000..064e2fc7434 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_monitoring.js @@ -0,0 +1,31 @@ +/** + * Renders the Monitoring (Metrics) link in environments table. + */ +export default { + props: { + monitoringUrl: { + type: String, + default: '', + required: true, + }, + }, + + computed: { + title() { + return 'Monitoring'; + }, + }, + + template: ` + <a + class="btn monitoring-url has-tooltip" + data-container="body" + :href="monitoringUrl" + target="_blank" + rel="noopener noreferrer nofollow" + :title="title" + :aria-label="title"> + <i class="fa fa-area-chart" aria-hidden="true"></i> + </a> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js index 5404d647745..47102692024 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -25,6 +25,12 @@ export default { }; }, + computed: { + title() { + return 'Stop'; + }, + }, + methods: { onClick() { if (confirm('Are you sure you want to stop this environment?')) { @@ -45,10 +51,12 @@ export default { template: ` <button type="button" - class="btn stop-env-link" + class="btn stop-env-link has-tooltip" + data-container="body" @click="onClick" :disabled="isLoading" - title="Stop Environment"> + :title="title" + :aria-label="title"> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> </button> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js index 66a71faa02f..092a50a0d6f 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js @@ -14,12 +14,22 @@ export default { }, data() { - return { terminalIconSvg }; + return { + terminalIconSvg, + }; + }, + + computed: { + title() { + return 'Terminal'; + }, }, template: ` - <a class="btn terminal-button" - title="Open web terminal" + <a class="btn terminal-button has-tooltip" + data-container="body" + :title="title" + :aria-label="title" :href="terminalPath"> ${terminalIconSvg} </a> diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index 9bf1b1ced88..a2729dc0e95 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -8,21 +8,31 @@ require('./filtered_search_token_keys'); // Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = []; + const tokenIndexes = []; // stores key+value for simple search let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { let tokenValue = v1 || v2 || v3; let tokenSymbol = symbol; + let tokenIndex = ''; if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { tokenSymbol = tokenValue; tokenValue = ''; } - tokens.push({ - key, - value: tokenValue || '', - symbol: tokenSymbol || '', - }); + tokenIndex = `${key}:${tokenValue}`; + + // Prevent adding duplicates + if (tokenIndexes.indexOf(tokenIndex) === -1) { + tokenIndexes.push(tokenIndex); + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + } + return ''; }).replace(/\s{2,}/g, ' ').trim() || ''; diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 6a028f299b1..62675d7e67e 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,40 +1,64 @@ -const GROUP_LIMIT = 2; + +import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title'); - this.groups = document.querySelectorAll('.group-path'); + this.titleContainer = document.querySelector('.title-container'); + this.title = document.querySelector('.title'); + this.titleWidth = this.title.offsetWidth; this.groupTitle = document.querySelector('.group-title'); + this.groups = document.querySelectorAll('.group-path'); this.toggle = null; this.isHidden = false; this.init(); } init() { - if (this.groups.length > GROUP_LIMIT) { + if (this.groups.length > 0) { this.groups[this.groups.length - 1].classList.remove('hidable'); - this.addToggle(); + this.toggleHandler(); + window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100)); } this.render(); } - addToggle() { - const header = document.querySelector('.header-content'); + toggleHandler() { + if (this.titleWidth > this.titleContainer.offsetWidth) { + if (!this.toggle) this.createToggle(); + this.showToggle(); + } else if (this.toggle) { + this.hideToggle(); + } + } + + createToggle() { this.toggle = document.createElement('button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.innerHTML = '...'; this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - header.insertBefore(this.toggle, this.titleContainer); + this.titleContainer.insertBefore(this.toggle, this.title); this.toggleGroups(); } + showToggle() { + this.title.classList.add('wrap'); + this.toggle.classList.remove('hidden'); + if (this.isHidden) this.groupTitle.classList.add('is-hidden'); + } + + hideToggle() { + this.title.classList.remove('wrap'); + this.toggle.classList.add('hidden'); + if (this.isHidden) this.groupTitle.classList.remove('is-hidden'); + } + toggleGroups() { this.isHidden = !this.isHidden; this.groupTitle.classList.toggle('is-hidden'); } render() { - this.titleContainer.classList.remove('initializing'); + this.title.classList.remove('initializing'); } } diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index e5dfa30edab..602a3b78189 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -6,23 +6,60 @@ var slice = [].slice; window.GroupsSelect = (function() { function GroupsSelect() { $('.ajax-groups-select').each((function(_this) { + const self = _this; + return function(i, select) { var all_available, skip_groups; - all_available = $(select).data('all-available'); - skip_groups = $(select).data('skip-groups') || []; - return $(select).select2({ + const $select = $(select); + all_available = $select.data('all-available'); + skip_groups = $select.data('skip-groups') || []; + + $select.select2({ placeholder: "Search for a group", - multiple: $(select).hasClass('multiselect'), + multiple: $select.hasClass('multiselect'), minimumInputLength: 0, - query: function(query) { - var options = { all_available: all_available, skip_groups: skip_groups }; - return Api.groups(query.term, options, function(groups) { - var data; - data = { - results: groups + ajax: { + url: Api.buildUrl(Api.groupsPath), + dataType: 'json', + quietMillis: 250, + transport: function (params) { + $.ajax(params).then((data, status, xhr) => { + const results = data || []; + + const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; + + return { + results, + pagination: { + more, + }, + }; + }).then(params.success).fail(params.error); + }, + data: function (search, page) { + return { + search, + page, + per_page: GroupsSelect.PER_PAGE, + all_available, + skip_groups, + }; + }, + results: function (data, page) { + if (data.length) return { results: [] }; + + const results = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + + return { + results, + page, + more, }; - return query.callback(data); - }); + }, }, initSelection: function(element, callback) { var id; @@ -34,19 +71,23 @@ window.GroupsSelect = (function() { formatResult: function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); + return self.formatResult.apply(self, args); }, formatSelection: function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); + return self.formatSelection.apply(self, args); }, - dropdownCssClass: "ajax-groups-dropdown", + dropdownCssClass: "ajax-groups-dropdown select2-infinite", // we do not want to escape markup since we are displaying html in results escapeMarkup: function(m) { return m; } }); + + self.dropdown = document.querySelector('.select2-infinite .select2-results'); + + $select.on('select2-loaded', self.forceOverflow.bind(self)); }; })(this)); } @@ -65,5 +106,12 @@ window.GroupsSelect = (function() { return group.full_name; }; + GroupsSelect.prototype.forceOverflow = function (e) { + const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight; + this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`; + }; + + GroupsSelect.PER_PAGE = 20; + return GroupsSelect; })(); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 34f44dad7a5..dc170c60456 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ $(document).on('todo:toggle', function(e, count) { - var $todoPendingCount = $('.todos-pending-count'); + var $todoPendingCount = $('.todos-count'); $todoPendingCount.text(gl.text.highCountTrim(count)); $todoPendingCount.toggleClass('hidden', count === 0); }); diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 08ca9e4fa4d..a5f99bcdd8f 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -11,8 +11,9 @@ }); }; - $(function() { - var $scrollingTabs = $('.scrolling-tabs'); + $(document).on('init.scrolling-tabs', () => { + const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); + $scrollingTabs.addClass('is-initialized'); hideEndFade($scrollingTabs); $(window).off('resize.nav').on('resize.nav', function() { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index a1423b6fda5..4aad0128aef 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -232,6 +232,22 @@ }; /** + this will take in the getAllResponseHeaders result and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys + */ + w.gl.utils.normalizeCRLFHeaders = (headers) => { + const headersObject = {}; + const headersArray = headers.split('\n'); + + headersArray.forEach((header) => { + const keyValue = header.split(': '); + headersObject[keyValue[0]] = keyValue[1]; + }); + + return w.gl.utils.normalizeHeaders(headersObject); + }; + + /** * Parses pagination object string values into numbers. * * @param {Object} paginationInformation diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index c30a1fcb5da..5c22aea51cd 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -5,23 +5,37 @@ import httpStatusCodes from './http_status'; * Service for vue resouce and method need to be provided as props * * @example - * new poll({ + * new Poll({ * resource: resource, * method: 'name', - * data: {page: 1, scope: 'all'}, + * data: {page: 1, scope: 'all'}, // optional * successCallback: () => {}, * errorCallback: () => {}, + * notificationCallback: () => {}, // optional * }).makeRequest(); * - * this.service = new BoardsService(endpoint); - * new poll({ - * resource: this.service, - * method: 'get', - * data: {page: 1, scope: 'all'}, - * successCallback: () => {}, - * errorCallback: () => {}, - * }).makeRequest(); + * Usage in pipelines table with visibility lib: * + * const poll = new Poll({ + * resource: this.service, + * method: 'getPipelines', + * data: { page: pageNumber, scope }, + * successCallback: this.successCallback, + * errorCallback: this.errorCallback, + * notificationCallback: this.updateLoading, + * }); + * + * if (!Visibility.hidden()) { + * poll.makeRequest(); + * } + * + * Visibility.change(() => { + * if (!Visibility.hidden()) { + * poll.restart(); + * } else { + * poll.stop(); + * } +* }); * * 1. Checks for response and headers before start polling * 2. Interval is provided by `Poll-Interval` header. @@ -34,6 +48,8 @@ export default class Poll { constructor(options = {}) { this.options = options; this.options.data = options.data || {}; + this.options.notificationCallback = options.notificationCallback || + function notificationCallback() {}; this.intervalHeader = 'POLL-INTERVAL'; this.timeoutID = null; @@ -42,7 +58,7 @@ export default class Poll { checkConditions(response) { const headers = gl.utils.normalizeHeaders(response.headers); - const pollInterval = headers[this.intervalHeader]; + const pollInterval = parseInt(headers[this.intervalHeader], 10); if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { this.timeoutID = setTimeout(() => { @@ -54,11 +70,14 @@ export default class Poll { } makeRequest() { - const { resource, method, data, errorCallback } = this.options; + const { resource, method, data, errorCallback, notificationCallback } = this.options; + + // It's called everytime a new request is made. Useful to update the status. + notificationCallback(true); return resource[method](data) - .then(response => this.checkConditions(response)) - .catch(error => errorCallback(error)); + .then(response => this.checkConditions(response)) + .catch(error => errorCallback(error)); } /** @@ -70,4 +89,12 @@ export default class Poll { this.canPoll = false; clearTimeout(this.timeoutID); } + + /** + * Restarts polling after it has been stoped + */ + restart() { + this.canPoll = true; + this.makeRequest(); + } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9007d661d01..665a59f3183 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -370,4 +370,6 @@ $(function () { new Aside(); gl.utils.initTimeagoTimeout(); + + $(document).trigger('init.scrolling-tabs'); }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 811f90c5a87..3c4e6102469 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -4,8 +4,10 @@ import Cookies from 'js-cookie'; -require('./breakpoints'); -require('./flash'); +import CommitPipelinesTable from './commit/pipelines/pipelines_table'; + +import './breakpoints'; +import './flash'; /* eslint-disable max-len */ // MergeRequestTabs @@ -97,6 +99,13 @@ require('./flash'); .off('click', this.clickTab); } + destroy() { + this.unbindEvents(); + if (this.commitPipelinesTable) { + this.commitPipelinesTable.$destroy(); + } + } + showTab(e) { e.preventDefault(); this.activateTab($(e.target).data('action')); @@ -128,12 +137,8 @@ require('./flash'); this.expandViewContainer(); } } else if (action === 'pipelines') { - if (this.pipelinesLoaded) { - return; - } - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); - this.pipelinesLoaded = true; + this.resetViewContainer(); + this.loadPipelines(); } else { this.expandView(); this.resetViewContainer(); @@ -222,6 +227,18 @@ require('./flash'); }); } + loadPipelines() { + if (this.pipelinesLoaded) { + return; + } + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + // Could already be mounted from the `pipelines_bundle` + if (pipelineTableViewEl) { + this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl); + } + this.pipelinesLoaded = true; + } + loadDiff(source) { if (this.diffsLoaded) { return; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index c38bc762675..4ccea0624ee 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -25,6 +25,7 @@ bindEvents() { $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('#user_notification_email').on('change', this.submitForm); + $('#user_notified_of_own_activity').on('change', this.submitForm); $('.update-username').on('ajax:before', this.beforeUpdateUsername); $('.update-username').on('ajax:complete', this.afterUpdateUsername); $('.update-notifications').on('ajax:success', this.onUpdateNotifs); diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js index 5cf28aa7a73..1d4bb8a13d6 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js @@ -6,7 +6,7 @@ class ProtectedBranchDropdown { this.$dropdown = options.$dropdown; this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); + this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch'); this.buildDropdown(); this.bindEvents(); @@ -46,7 +46,9 @@ class ProtectedBranchDropdown { this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); } - onClickCreateWildcard() { + onClickCreateWildcard(e) { + e.preventDefault(); + // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').selectRowAtIndex(); @@ -69,7 +71,7 @@ class ProtectedBranchDropdown { if (branchName) { this.$dropdownContainer - .find('.create-new-protected-branch code') + .find('.js-create-new-protected-branch code') .text(branchName); } diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 64a68d56962..a9b3de281e1 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -56,14 +56,15 @@ import Cookies from 'js-cookie'; Sidebar.prototype.toggleTodo = function(e) { var $btnText, $this, $todoLoading, ajaxType, url; $this = $(e.currentTarget); - $todoLoading = $('.js-issuable-todo-loading'); - $btnText = $('.js-issuable-todo-text', $this); ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; if ($this.attr('data-delete-path')) { url = "" + ($this.attr('data-delete-path')); } else { url = "" + ($this.data('url')); } + + $this.tooltip('hide'); + return $.ajax({ url: url, type: ajaxType, @@ -74,34 +75,44 @@ import Cookies from 'js-cookie'; }, beforeSend: (function(_this) { return function() { - return _this.beforeTodoSend($this, $todoLoading); + $('.js-issuable-todo').disable() + .addClass('is-loading'); }; })(this) }).done((function(_this) { return function(data) { - return _this.todoUpdateDone(data, $this, $btnText, $todoLoading); + return _this.todoUpdateDone(data); }; })(this)); }; - Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) { - $btn.disable(); - return $todoLoading.removeClass('hidden'); - }; + Sidebar.prototype.todoUpdateDone = function(data) { + const deletePath = data.delete_path ? data.delete_path : null; + const attrPrefix = deletePath ? 'mark' : 'todo'; + const $todoBtns = $('.js-issuable-todo'); - Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) { $(document).trigger('todo:toggle', data.count); - $btn.enable(); - $todoLoading.addClass('hidden'); + $todoBtns.each((i, el) => { + const $el = $(el); + const $elText = $el.find('.js-issuable-todo-inner'); - if (data.delete_path != null) { - $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path); - return $btnText.text($btn.data('mark-text')); - } else { - $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path'); - return $btnText.text($btn.data('todo-text')); - } + $el.removeClass('is-loading') + .enable() + .attr('aria-label', $el.data(`${attrPrefix}-text`)) + .attr('data-delete-path', deletePath) + .attr('title', $el.data(`${attrPrefix}-text`)); + + if ($el.hasClass('has-tooltip')) { + $el.tooltip('fixTitle'); + } + + if ($el.data(`${attrPrefix}-icon`)) { + $elText.html($el.data(`${attrPrefix}-icon`)); + } else { + $elText.text($el.data(`${attrPrefix}-text`)); + } + }); }; Sidebar.prototype.sidebarDropdownLoading = function(e) { @@ -198,7 +209,7 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight(); const $rightSidebar = $('.js-right-sidebar'); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index b27d252a3ef..fa078b48bf8 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,57 +1,26 @@ import Cookies from 'js-cookie'; -const userCalloutElementName = '.user-callout'; -const closeButton = '.close-user-callout'; -const userCalloutBtn = '.user-callout-btn'; -const userCalloutSvgAttrName = 'callout-svg'; - const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; -const USER_CALLOUT_TEMPLATE = ` - <div class="bordered-box landing content-block"> - <button class="btn btn-default close close-user-callout" type="button"> - <i class="fa fa-times dismiss-icon"></i> - </button> - <div class="row"> - <div class="col-sm-3 col-xs-12 svg-container"> - </div> - <div class="col-sm-8 col-xs-12 inner-content"> - <h4> - Customize your experience - </h4> - <p> - Change syntax themes, default project pages, and more in preferences. - </p> - <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a> - </div> - </div> -</div>`; - export default class UserCallout { constructor() { this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); - this.userCalloutBody = $(userCalloutElementName); - this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName); - $(userCalloutElementName).removeAttr(userCalloutSvgAttrName); + this.userCalloutBody = $('.user-callout'); this.init(); } init() { - const $template = $(USER_CALLOUT_TEMPLATE); if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { - $template.find('.svg-container').append(this.userCalloutSvg); - this.userCalloutBody.append($template); - $template.find(closeButton).on('click', e => this.dismissCallout(e)); - $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); - } else { - this.userCalloutBody.remove(); + $('.js-close-callout').on('click', e => this.dismissCallout(e)); } } dismissCallout(e) { - Cookies.set(USER_CALLOUT_COOKIE, 'true'); const $currentTarget = $(e.currentTarget); - if ($currentTarget.hasClass('close-user-callout')) { + + Cookies.set(USER_CALLOUT_COOKIE, 'true'); + + if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); } } diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js index aaebf29d8ae..58b8db4d519 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/async_button.js +++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js @@ -83,6 +83,7 @@ export default { :class="buttonClass" :title="title" :aria-label="title" + data-container="body" data-placement="top" :disabled="isLoading"> <i :class="iconClass" aria-hidden="true"/> diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js index b4480bd98c7..1626ae17a30 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js +++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js @@ -16,8 +16,12 @@ export default { }, }, + mounted() { + $(document).trigger('init.scrolling-tabs'); + }, + template: ` - <ul class="nav-links"> + <ul class="nav-links scrolling-tabs"> <li class="js-pipelines-tab-all" :class="{ 'active': scope === 'all'}"> diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js index 3555040d60f..f18e2dfadaf 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js +++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js @@ -21,6 +21,7 @@ export default { <li v-for="artifact in artifacts"> <a rel="nofollow" + download :href="artifact.path"> <i class="fa fa-download" aria-hidden="true"></i> <span>Download {{artifact.name}} artifacts</span> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js index 48f0e9036e8..9bdc232b7da 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -182,8 +182,14 @@ export default { <div :class="cssClass"> <div - class="top-area" + class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> + <div class="fade-left"> + <i class="fa fa-angle-left" aria-hidden="true"></i> + </div> + <div class="fade-right"> + <i class="fa fa-angle-right" aria-hidden="true"></i> + </div> <navigation-tabs :scope="scope" :count="state.count" diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js index b9cd28f6249..ebb14912b00 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js @@ -3,8 +3,8 @@ const UI_LIMIT = 6; const SPREAD = '...'; const PREV = 'Prev'; const NEXT = 'Next'; -const FIRST = '<< First'; -const LAST = 'Last >>'; +const FIRST = 'ยซ First'; +const LAST = 'Last ยป'; export default { props: { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 50849e95541..4369ae78bde 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -362,3 +362,13 @@ width: 100%; } } + +.btn-blank { + padding: 0; + background: transparent; + border: 0; + + &:focus { + outline: 0; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index da5b754aec7..2ede47e9de6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -119,6 +119,46 @@ } } +@mixin dropdown-link { + display: block; + position: relative; + padding: 5px 8px; + color: $gl-text-color; + line-height: initial; + text-overflow: ellipsis; + border-radius: 2px; + white-space: nowrap; + overflow: hidden; + + &:hover, + &:focus, + &.is-focused { + background-color: $dropdown-link-hover-bg; + text-decoration: none; + + .badge { + background-color: darken($dropdown-link-hover-bg, 5%); + } + } + + &.dropdown-menu-empty-link { + &.is-focused { + background-color: $dropdown-empty-row-bg; + } + } + + &.dropdown-menu-user-link { + line-height: 16px; + } + + .icon-play { + fill: $gl-text-color-secondary; + margin-right: 6px; + height: 12px; + width: 11px; + } +} + .dropdown-menu, .dropdown-menu-nav { display: none; @@ -178,43 +218,7 @@ } a { - display: block; - position: relative; - padding: 5px 8px; - color: $gl-text-color; - line-height: initial; - text-overflow: ellipsis; - border-radius: 2px; - white-space: nowrap; - overflow: hidden; - - &:hover, - &:focus, - &.is-focused { - background-color: $dropdown-link-hover-bg; - text-decoration: none; - - .badge { - background-color: darken($dropdown-link-hover-bg, 5%); - } - } - - &.dropdown-menu-empty-link { - &.is-focused { - background-color: $dropdown-empty-row-bg; - } - } - - &.dropdown-menu-user-link { - line-height: 16px; - } - - .icon-play { - fill: $gl-text-color-secondary; - margin-right: 6px; - height: 12px; - width: 11px; - } + @include dropdown-link; } .dropdown-header { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index fa02598760f..abb092623c0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -26,7 +26,7 @@ header { padding: 0 16px; z-index: 100; margin-bottom: 0; - height: $header-height; + min-height: $header-height; background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -48,10 +48,10 @@ header { color: $gl-text-color-secondary; font-size: 18px; padding: 0; - margin: ($header-height - 28) / 2 0; + margin: (($header-height - 28) / 2) 3px; margin-left: 8px; height: 28px; - min-width: 28px; + min-width: 32px; line-height: 28px; text-align: center; @@ -73,21 +73,29 @@ header { background-color: $gray-light; color: $gl-text-color; - .todos-pending-count { - background: darken($todo-alert-blue, 10%); + svg { + fill: $gl-text-color; } } .fa-caret-down { font-size: 14px; } + + svg { + position: relative; + top: 2px; + height: 17px; + // hack to get SVG to line up with FA icons + width: 23px; + fill: $gl-text-color-secondary; + } } .navbar-toggle { color: $nav-toggle-gray; - margin: 6px 0; + margin: 5px 0; border-radius: 0; - position: absolute; right: -10px; padding: 6px 10px; @@ -135,14 +143,12 @@ header { } .header-content { + display: flex; + justify-content: space-between; position: relative; - height: $header-height; + min-height: $header-height; padding-left: 30px; - @media (min-width: $screen-sm-min) { - padding-right: 0; - } - .dropdown-menu { margin-top: -5px; } @@ -165,8 +171,7 @@ header { } .group-name-toggle { - margin: 0 5px; - vertical-align: sub; + margin: 3px 5px; } .group-title { @@ -177,39 +182,32 @@ header { } } + .title-container { + display: flex; + align-items: flex-start; + flex: 1 1 auto; + padding-top: (($header-height - 19) / 2); + overflow: hidden; + } + .title { position: relative; padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; + line-height: 22px; display: inline-block; - line-height: $header-height; font-weight: normal; color: $gl-text-color; - overflow: hidden; - text-overflow: ellipsis; vertical-align: top; white-space: nowrap; - &.initializing { - display: none; - } - - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - - @media (max-width: $screen-xs-max) { - max-width: 190px; + &.wrap { + white-space: normal; } - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { - max-width: 428px; - } - - @media (min-width: $screen-lg-min) { - max-width: 685px; + &.initializing { + opacity: 0; } a { @@ -226,10 +224,10 @@ header { border: transparent; background: transparent; position: absolute; + top: 2px; right: 3px; width: 12px; line-height: 19px; - margin-top: (($header-height - 19) / 2); padding: 0; font-size: 10px; text-align: center; @@ -247,15 +245,12 @@ header { } .navbar-collapse { - float: right; + flex: 0 0 auto; border-top: none; - - @media (min-width: $screen-md-min) { - padding: 0; - } + padding: 0; @media (max-width: $screen-xs-max) { - float: none; + flex: 1 1 auto; } } } @@ -269,10 +264,30 @@ header { } } -.page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-md-max) { - .header-content .title { - width: 300px; +.navbar-nav { + li { + .badge { + position: inherit; + top: -3px; + font-weight: normal; + margin-left: -12px; + font-size: 11px; + color: $white-light; + padding: 1px 5px 2px; + border-radius: 7px; + box-shadow: 0 1px 0 rgba($gl-header-color, .2); + + &.issues-count { + background-color: $green-500; + } + + &.merge-requests-count { + background-color: $orange-600; + } + + &.todos-count { + background-color: $blue-500; + } } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index df78bbdea51..b3340d41333 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -52,6 +52,18 @@ } } +@mixin basic-list-stats { + .stats { + float: right; + line-height: $list-text-height; + color: $gl-text-color; + + span { + margin-right: 15px; + } + } +} + @mixin bulleted-list { > ul { list-style-type: disc; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 5ab505034b6..e6d808717f3 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -146,6 +146,10 @@ display: block; } + &.scrolling-tabs { + float: left; + } + li a { padding: 16px 15px 11px; } @@ -476,3 +480,44 @@ } } } + +.inner-page-scroll-tabs { + position: relative; + + .nav-links { + padding-bottom: 1px; + } + + .fade-right { + @include fade(left, $white-light); + right: 0; + text-align: right; + + .fa { + right: 5px; + } + } + + .fade-left { + @include fade(right, $white-light); + left: 0; + text-align: left; + + .fa { + left: 5px; + } + } + + .fade-right, + .fade-left { + top: 16px; + bottom: auto; + } + + &.is-smaller { + .fade-right, + .fade-left { + top: 11px; + } + } +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 40e93032f59..746c9c25620 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -33,7 +33,7 @@ padding-right: 0; @media (min-width: $screen-sm-min) { - .content-wrapper { + &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } @@ -55,7 +55,7 @@ padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - .content-wrapper { + &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 575d32b1a23..b6168a293e0 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -240,8 +240,13 @@ font-size: (14px / $issue-boards-font-size) * 1em; } + .card-assignee { + margin-right: 5px; + } + .avatar { margin-left: 0; + margin-right: 0; } } @@ -296,7 +301,7 @@ } } -.issue-boards-sidebar { +.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { &.right-sidebar { top: 0; bottom: 0; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index da8410eca66..0dad91ba128 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -142,7 +142,9 @@ border: 1px solid $border-gray-dark; border-radius: $border-radius-default; margin-left: 5px; - line-height: 1; + font-size: $gl-font-size; + line-height: $gl-font-size; + outline: none; &:hover { background-color: darken($gray-light, 10%); diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index eab79c2a481..1aa1079903c 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -431,6 +431,21 @@ border-bottom: none; } +.diff-stats-summary-toggler { + padding: 0; + background-color: transparent; + border: 0; + color: $gl-link-color; + transition: color 0.1s linear; + + &:hover, + &:focus { + outline: none; + text-decoration: underline; + color: $gl-link-hover-color; + } +} + // Mobile @media (max-width: 480px) { .diff-title { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 25be7f408d0..3d91e0b22d8 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -26,7 +26,7 @@ .table.ci-table { .environments-actions { - min-width: 200px; + min-width: 300px; } .environments-commit, @@ -222,3 +222,12 @@ stroke: $black; stroke-width: 1; } + +.environments-actions { + .external-url, + .monitoring-url, + .terminal-button, + .stop-env-link { + width: 38px; + } +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 84d21e48463..73a5889867a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -9,16 +9,15 @@ } } -.group-row { - .stats { - float: right; - line-height: $list-text-height; - color: $gl-text-color; +.group-root-path { + max-width: 40vw; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: nowrap; +} - span { - margin-right: 15px; - } - } +.group-row { + @include basic-list-stats; } .ldap-group-links { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index c1a9bc4be28..e84a05e3e9e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -243,6 +243,10 @@ font-size: 13px; font-weight: normal; } + + .hide-expanded { + display: none; + } } &.right-sidebar-collapsed { @@ -282,10 +286,11 @@ display: block; width: 100%; text-align: center; - padding-bottom: 10px; + margin-bottom: 10px; color: $issuable-sidebar-color; - &:hover { + &:hover, + &:hover .todo-undone { color: $gl-text-color; } @@ -294,6 +299,10 @@ margin-top: 0; } + .todo-undone { + color: $gl-link-color; + } + .author { display: none; } @@ -582,3 +591,21 @@ opacity: 0; } } + +.issuable-todo-btn { + .fa-spinner { + display: none; + } + + &.is-loading { + .fa-spinner { + display: inline-block; + } + + &.sidebar-collapsed-icon { + .issuable-todo-inner { + display: none; + } + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6630904ec92..566dcc64802 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -60,7 +60,17 @@ } .modify-merge-commit-link { + padding: 0; + + background-color: transparent; + border: 0; + color: $gl-text-color; + + &:hover, + &:focus { + text-decoration: underline; + } } .merge-param-checkbox { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index efbd9365fd9..335e587b8f4 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -52,66 +52,62 @@ } } -.milestone-summary { - .milestone-stat { - white-space: nowrap; - margin-right: 10px; +.milestone-sidebar { + .gutter-toggle { + margin-bottom: 10px; + } - &.with-drilldown { - margin-right: 2px; + .milestone-progress { + .title { + padding-top: 5px; } - } - .remaining-days { - color: $orange-600; + .progress { + height: 6px; + margin: 0; + } } - .milestone-stats-and-buttons { - display: flex; - justify-content: flex-start; - flex-wrap: wrap; + .collapsed-milestone-date { + font-size: 12px; + } - @media (min-width: $screen-xs-min) { - justify-content: space-between; - flex-wrap: nowrap; - } + .milestone-date { + display: block; } - .milestone-progress-buttons { - order: 1; - margin-top: 10px; + .date-separator { + line-height: 5px; + } - @media (min-width: $screen-xs-min) { - order: 2; - margin-top: 0; - flex-shrink: 0; - } + .remaining-days strong { + font-weight: normal; + } - .btn { - float: left; - margin-right: $btn-side-margin; + .milestone-stat { + float: left; + margin-right: 14px; + } - &:last-child { - margin-right: 0; - } - } + .milestone-stat:last-child { + margin-right: 0; } - .milestone-stats { - order: 2; - width: 100%; - padding: 7px 0; - flex-shrink: 1; + .milestone-progress { + .sidebar-collapsed-icon { + clear: both; + padding: 15px 5px 5px; - @media (min-width: $screen-xs-min) { - // when displayed on one line stats go first, buttons second - order: 1; + .progress { + margin: 5px 0; + } } } - .progress { - width: 100%; - margin: 15px 0; + .right-sidebar-collapsed & { + .reference { + border-top: 1px solid $border-gray-normal; + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a2129722633..57cf8e136e2 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -243,22 +243,6 @@ ul.notes { } } -.page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-md-max) { - .note-header { - .note-headline-light { - display: block; - } - - .note-actions { - position: absolute; - right: 0; - top: 0; - } - } - } -} - // Diff code in discussion view .discussion-body .diff-file { .file-title { @@ -426,8 +410,22 @@ ul.notes { } .discussion-toggle-button { + padding: 0; + background-color: transparent; + border: 0; line-height: 20px; font-size: 13px; + transition: color 0.1s linear; + + &:hover { + color: $gl-link-color; + } + + &:focus { + text-decoration: underline; + outline: none; + color: $gl-link-color; + } .fa { margin-right: 3px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 949d52cffa2..c2c2f371b87 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -477,20 +477,6 @@ a.deploy-project-label { } } -.page-sidebar-pinned { - .project-stats .nav > li.right { - @media (min-width: $screen-lg-min) { - float: none; - } - } - - .download-button { - @media (min-width: $screen-lg-min) { - margin-left: 0; - } - } -} - .project-stats { font-size: 0; text-align: center; @@ -587,9 +573,19 @@ pre.light-well { display: flex; flex-direction: column; + // Disable Flexbox for admin page + &.admin-projects { + display: block; + + .project-row { + display: block; + } + } + .project-row { display: flex; align-items: center; + @include basic-list-stats; } h3 { @@ -746,6 +742,15 @@ pre.light-well { } } +.create-new-protected-branch-button { + @include dropdown-link; + + width: 100%; + background-color: transparent; + border: 0; + text-align: left; +} + .protected-branches-list { margin-bottom: 30px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index b071d7f18cd..a39815319f3 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -3,25 +3,6 @@ * */ -.navbar-nav { - li { - .badge.todos-pending-count { - position: inherit; - top: -6px; - margin-top: -5px; - font-weight: normal; - background: $todo-alert-blue; - margin-left: -17px; - font-size: 11px; - color: $white-light; - padding: 3px; - padding-top: 1px; - padding-bottom: 1px; - border-radius: 3px; - } - } -} - .todos-list > .todo { // workaround because we cannot use border-colapse border-top: 1px solid transparent; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8d831ffdd70..0bfbe47eb4f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def application_setting_params - restricted_levels = params[:application_setting][:restricted_visibility_levels] - if restricted_levels.nil? - params[:application_setting][:restricted_visibility_levels] = [] - else - restricted_levels.map! do |level| - level.to_i - end - end - import_sources = params[:application_setting][:import_sources] if import_sources.nil? params[:application_setting][:import_sources] = [] diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index c09095b9849..5f90ad7137d 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -1,7 +1,7 @@ class Admin::BackgroundJobsController < Admin::ApplicationController def show - ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) - @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) + ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) + @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/) @concurrency = Sidekiq.options[:concurrency] end end diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb index d496f08a598..4531657268c 100644 --- a/app/controllers/admin/labels_controller.rb +++ b/app/controllers/admin/labels_controller.rb @@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController end def create - @label = Label.new(label_params) - @label.template = true + @label = Labels::CreateService.new(label_params).execute(template: true) - if @label.save + if @label.persisted? redirect_to admin_labels_url, notice: "Label was created" else render :new @@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController end def update - if @label.update(label_params) + @label = Labels::UpdateService.new(label_params).execute(@label) + + if @label.valid? redirect_to admin_labels_path, notice: 'label was successfully updated.' else render :edit diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 587898a8634..facb25525b5 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController end def create - @label = @group.labels.create(label_params) + @label = Labels::CreateService.new(label_params).execute(group: group) if @label.valid? redirect_to group_labels_path(@group) @@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController end def update - if @label.update_attributes(label_params) + @label = Labels::UpdateService.new(label_params).execute(@label) + + if @label.valid? redirect_back_or_group_labels_path else render :edit diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 256c41e6145..eeee027ef2d 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController namespace.add_owner(current_user) namespace rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid - Namespace.find_by_path_or_name(name) + Namespace.find_by_full_path(name) end end end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 69959fe3687..7d1aa8d1ce0 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,11 +1,22 @@ class Profiles::AccountsController < Profiles::ApplicationController + include AuthHelper + def show @user = current_user end def unlink provider = params[:provider] - current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml' + identity = current_user.identities.find_by(provider: provider) + + return render_404 unless identity + + if unlink_allowed?(provider) + identity.destroy + else + flash[:alert] = "You are not allowed to unlink your primary login account" + end + redirect_to profile_account_path end end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index b8b71d295f6..a271e2dfc4b 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def user_params - params.require(:user).permit(:notification_email) + params.require(:user).permit(:notification_email, :notified_of_own_activity) end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index f1e4246e7fb..3f3c90a49ab 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController end def status - render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + render json: BuildSerializer + .new(project: @project, user: @current_user) + .represent_status(@build) end def erase diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 1593b5c1afb..2f55ba4e700 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController end def create - @label = @project.labels.create(label_params) + @label = Labels::CreateService.new(label_params).execute(project: @project) if @label.valid? respond_to do |format| @@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController end def update - if @label.update_attributes(label_params) + @label = Labels::UpdateService.new(label_params).execute(@label) + + if @label.valid? redirect_to namespace_project_labels_path(@project.namespace, @project) else render :edit diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2fadf7c8c81..9621b30b251 100755 --- 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, :conflict_for_path, :pipelines, :merge, :merge_check, - :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues + :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] @@ -97,31 +97,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs apply_diff_view_cookie! - @merge_request_diff = - if params[:diff_id] - @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) - else - @merge_request.merge_request_diff - end - - @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff - @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } - - if params[:start_sha].present? - @start_sha = params[:start_sha] - @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } - - unless @start_version - @start_sha = @merge_request_diff.head_commit_sha - @start_version = @merge_request_diff - end - end - - @environment = @merge_request.environments_for(current_user).last - respond_to do |format| format.html { define_discussion_vars } format.json do + @merge_request_diff = + if params[:diff_id] + @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) + else + @merge_request.merge_request_diff + end + + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff + @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } + + if params[:start_sha].present? + @start_sha = params[:start_sha] + @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } + + unless @start_version + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff + end + end + + @environment = @merge_request.environments_for(current_user).last + if @start_sha compared_diff_version else @@ -473,6 +473,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def pipeline_status + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent_status(@merge_request.head_pipeline) + end + def ci_environments_status environments = begin diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 718d9e86bea..43a1abaa662 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def status + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent_status(@pipeline) + end + def stage @stage = pipeline.stage(params[:stage]) return not_found unless @stage diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index f210f7e61d2..c5e24b9e365 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -124,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController end def wiki_params - params[:wiki].slice(:title, :content, :format, :message) + params.require(:wiki).permit(:title, :content, :format, :message) end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 612d69cf557..4a579601785 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -6,45 +6,19 @@ class SearchController < ApplicationController layout 'search' def show - if params[:project_id].present? - @project = Project.find_by(id: params[:project_id]) - @project = nil unless can?(current_user, :download_code, @project) - end + search_service = SearchService.new(current_user, params) - if params[:group_id].present? - @group = Group.find_by(id: params[:group_id]) - @group = nil unless can?(current_user, :read_group, @group) - end + @project = search_service.project + @group = search_service.group return if params[:search].blank? @search_term = params[:search] - @scope = params[:scope] - @show_snippets = params[:snippets].eql? 'true' - - @search_results = - if @project - unless %w(blobs notes issues merge_requests milestones wiki_blobs - commits).include?(@scope) - @scope = 'blobs' - end - - Search::ProjectService.new(@project, current_user, params).execute - elsif @show_snippets - unless %w(snippet_blobs snippet_titles).include?(@scope) - @scope = 'snippet_blobs' - end - - Search::SnippetService.new(current_user, params).execute - else - unless %w(projects issues merge_requests milestones).include?(@scope) - @scope = 'projects' - end - Search::GlobalService.new(current_user, params).execute - end - - @search_objects = @search_results.objects(@scope, params[:page]) + @scope = search_service.scope + @show_snippets = search_service.show_snippets? + @search_results = search_service.search_results + @search_objects = search_service.search_objects check_single_commit_result end diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb new file mode 100644 index 00000000000..24c84d2d1aa --- /dev/null +++ b/app/finders/group_finder.rb @@ -0,0 +1,17 @@ +class GroupFinder + include Gitlab::Allowable + + def initialize(current_user) + @current_user = current_user + end + + def execute(*params) + group = Group.find_by(*params) + + if can?(@current_user, :read_group, group) + group + else + nil + end + end +end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index fa0e2a5e3d8..e52083f86e4 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder if project? if project - label_ids << project.group.labels if project.group.present? - label_ids << project.labels + if project.group.present? + labels_table = Label.arel_table + + label_ids << Label.where( + labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( + labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) + ) + ) + else + label_ids << project.labels + end end else label_ids << Label.where(group_id: projects.group_ids) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a3213581498..e5b811f3300 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -306,4 +306,8 @@ module ApplicationHelper def active_when(condition) 'active' if condition end + + def show_user_callout? + cookies[:user_callout_dismissed] == 'true' + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 1ee6c1d3afa..101fe579da2 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -76,5 +76,9 @@ module AuthHelper (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current end + def unlink_allowed?(provider) + %w(saml cas3).exclude?(provider.to_s) + end + extend self end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index a777db2826b..ec57fec4f99 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -251,6 +251,21 @@ module IssuablesHelper end def selected_template(issuable) - params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template]) + params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] } + end + + def issuable_todo_button_data(issuable, todo, is_collapsed) + { + todo_text: "Add todo", + mark_text: "Mark done", + todo_icon: (is_collapsed ? icon('plus-square') : nil), + mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil), + issuable_id: issuable.id, + issuable_type: issuable.class.name.underscore, + url: namespace_project_todos_path(@project.namespace, @project), + delete_path: (dashboard_todo_path(todo) if todo), + placement: (is_collapsed ? 'left' : nil), + container: (is_collapsed ? 'body' : nil) + } end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index bd3f51fc658..c9e70faa52e 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -19,8 +19,8 @@ module MilestonesHelper end end - def milestones_browse_issuables_path(milestone, type:) - opts = { milestone_title: milestone.title } + def milestones_browse_issuables_path(milestone, state: nil, type:) + opts = { milestone_title: milestone.title, state: state } if @project polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 991fd949b94..17bfd07e00f 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -6,7 +6,8 @@ module NavHelper current_path?('merge_requests#builds') || current_path?('merge_requests#conflicts') || current_path?('merge_requests#pipelines') || - current_path?('issues#show') + current_path?('issues#show') || + current_path?('milestones#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" else diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index b5017080cfb..55f4da0ef85 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -3,9 +3,9 @@ module SidekiqHelper (?<pid>\d+)\s+ (?<cpu>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+ - (?<state>[DRSTWXZNLsl\+<]+)\s+ - (?<start>.+)\s+ - (?<command>sidekiq.*\]) + (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+ + (?<start>.+?)\s+ + (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*) \z/x def parse_sidekiq_ps(line) diff --git a/app/models/blob.rb b/app/models/blob.rb index 1376b86fdad..95d2111a992 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -46,6 +46,10 @@ class Blob < SimpleDelegator text? && language && language.name == 'SVG' end + def ipython_notebook? + text? && language&.name == 'Jupyter Notebook' + end + def size_within_svg_limits? size <= MAXIMUM_SVG_SIZE end @@ -63,6 +67,8 @@ class Blob < SimpleDelegator end elsif image? || svg? 'image' + elsif ipython_notebook? + 'notebook' elsif text? 'text' else diff --git a/app/models/board.rb b/app/models/board.rb index 2780acc67c0..cf8317891b5 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,7 +5,7 @@ class Board < ActiveRecord::Base validates :project, presence: true - def done_list - lists.merge(List.done).take + def closed_list + lists.merge(List.closed).take end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f12be98c80c..ad7e0b23ff4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -210,7 +210,7 @@ module Ci end def stuck? - builds.pending.any?(&:stuck?) + builds.pending.includes(:project).any?(&:stuck?) end def retryable? diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8c71267da65..17b322b5ae3 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base end end + def locking_enabled? + status_changed? + end + def before_sha pipeline.before_sha || Gitlab::Git::BLANK_SHA end diff --git a/app/models/list.rb b/app/models/list.rb index 1e5da7f4dd4..fbd19acd1f5 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { label: 1, done: 2 } + enum list_type: { label: 1, closed: 2 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 1e0bbe69eb1..2c3473f9186 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base # Move the namespace directory in all storages paths used by member projects repository_storage_paths.each do |repository_storage_path| # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage_path, path_was) + gitlab_shell.add_namespace(repository_storage_path, full_path_was) - unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) - Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs @@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base end end - Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) - Gitlab::PagesTransfer.new.rename_namespace(path_was, path) + Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) + Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) remove_exports! @@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base def send_update_instructions projects.each do |project| - project.send_move_instructions("#{path_was}/#{project.path}") + project.send_move_instructions("#{full_path_was}/#{project.path}") end end @@ -230,10 +230,10 @@ class Namespace < ActiveRecord::Base old_repository_storage_paths.each do |repository_storage_path| # Move namespace directory into trash. # We will remove it later async - new_path = "#{path}+#{id}+deleted" + new_path = "#{full_path}+#{id}+deleted" - if gitlab_shell.mv_namespace(repository_storage_path, path, new_path) - message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path) + message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\"" Gitlab::AppLogger.info message # Remove namespace directroy async with delay so diff --git a/app/models/note.rb b/app/models/note.rb index e22e96aec6f..16d66cb1427 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -37,6 +37,7 @@ class Note < ActiveRecord::Base has_many :todos, dependent: :destroy has_many :events, as: :target, dependent: :destroy + has_one :system_note_metadata delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true @@ -70,7 +71,9 @@ class Note < ActiveRecord::Base scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } - scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) } + scope :inc_relations_for_view, -> do + includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata) + end scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) } diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index eef403dba92..3b90fd1c2c7 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -62,7 +62,7 @@ class JiraService < IssueTrackerService def help "You need to configure JIRA before enabling this service. For more details read the - [JIRA service documentation](#{help_page_url('project_services/jira')})." + [JIRA service documentation](#{help_page_url('user/project/integrations/jira')})." end def title diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 5cff9a42484..6854d2243d7 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -31,7 +31,7 @@ class PrometheusService < MonitoringService def help <<-MD.strip_heredoc - Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total` + Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) @@ -74,8 +74,8 @@ class PrometheusService < MonitoringService def calculate_reactive_cache(environment_slug) return unless active? && project && !project.pending_delete? - memory_query = %{(sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})) /1024/1024} - cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100} + memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024} + cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100} { success: true, diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 8a53e974b6f..6d6644053f8 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -169,6 +169,9 @@ class ProjectTeam # Lookup only the IDs we need user_ids = user_ids - access.keys + + return access if user_ids.empty? + users_access = project.project_authorizations. where(user: user_ids). group(:user_id). diff --git a/app/models/repository.rb b/app/models/repository.rb index 6ab04440ca8..596650353fc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -981,7 +981,13 @@ class Repository end def is_ancestor?(ancestor_id, descendant_id) - merge_base(ancestor_id, descendant_id) == ancestor_id + Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| + if is_enabled + raw_repository.is_ancestor?(ancestor_id, descendant_id) + else + merge_base_commit(ancestor_id, descendant_id) == ancestor_id + end + end end def empty_repo? diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb new file mode 100644 index 00000000000..5cc66574941 --- /dev/null +++ b/app/models/system_note_metadata.rb @@ -0,0 +1,11 @@ +class SystemNoteMetadata < ActiveRecord::Base + ICON_TYPES = %w[ + commit merge confidentiality status label assignee cross_reference + title time_tracking branch milestone discussion task moved + ].freeze + + validates :note, presence: true + validates :action, inclusion: ICON_TYPES, allow_nil: true + + belongs_to :note +end diff --git a/app/models/user.rb b/app/models/user.rb index 612066654dc..95a766f2ede 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,7 @@ class User < ActiveRecord::Base default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false default_value_for :project_view, :files + default_value_for :notified_of_own_activity, false attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, @@ -634,8 +635,10 @@ class User < ActiveRecord::Base end def fork_of(project) - links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects) - + links = ForkedProjectLink.where( + forked_from_project_id: project, + forked_to_project_id: personal_projects.unscope(:order) + ) if links.any? links.first.forked_to_project else diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index 5bcbe285052..fadd6c5c597 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity expose :created_at expose :updated_at + expose :detailed_status, as: :status, with: StatusEntity private + alias_method :build, :object + def path_to(route, build) send("#{route}_path", build.project.namespace, build.project, build) end + + def detailed_status + build.detailed_status(request.user) + end end diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb new file mode 100644 index 00000000000..79b67001199 --- /dev/null +++ b/app/serializers/build_serializer.rb @@ -0,0 +1,8 @@ +class BuildSerializer < BaseSerializer + entity BuildEntity + + def represent_status(resource) + data = represent(resource, { only: [:status] }) + data.fetch(:status, {}) + end +end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 4c017960628..4ff15a78115 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity expose :last_deployment, using: DeploymentEntity expose :stop_action? + expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| + metrics_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + expose :environment_path do |environment| namespace_project_environment_path( environment.project.namespace, diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 61f0f11d7d2..3f16dd66d54 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity end expose :details do - expose :status do |pipeline, options| - StatusEntity.represent( - pipeline.detailed_status(request.user), - options) - end - + expose :detailed_status, as: :status, with: StatusEntity expose :duration expose :finished_at expose :stages, using: StageEntity @@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity pipeline.cancelable? && can?(request.user, :update_pipeline, pipeline) end + + def detailed_status + pipeline.detailed_status(request.user) + end end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index ab2d3d5a3ec..7829df9fada 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer super(resource, opts) end end + + def represent_status(resource) + return {} unless resource.present? + + data = represent(resource, { only: [{ details: [:status] }] }) + data.dig(:details, :status) || {} + end end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 47066bebfb1..dfd9d1584a1 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -1,7 +1,7 @@ class StatusEntity < Grape::Entity include RequestAwareEntity - expose :icon, :text, :label, :group + expose :icon, :favicon, :text, :label, :group expose :has_details?, as: :has_details expose :details_path diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index f6275a63109..fd9ff115eab 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,7 +12,7 @@ module Boards def create_board! board = project.boards.create - board.lists.create(list_type: :done) + board.lists.create(list_type: :closed) board end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index cb6d30396ec..533e6787855 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -41,7 +41,7 @@ module Boards end def set_state - params[:state] = list && list.done? ? 'closed' : 'opened' + params[:state] = list && list.closed? ? '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 2a9981ab884..d5735f13c1e 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -48,8 +48,8 @@ module Boards end def issue_state - return 'reopen' if moving_from_list.done? - return 'close' if moving_to_list.done? + return 'reopen' if moving_from_list.closed? + return 'close' if moving_to_list.closed? end def add_label_ids diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 574561adc4c..f72ddbf690c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -7,14 +7,14 @@ module Ci raise Gitlab::Access::AccessDeniedError end - pipeline.builds.failed_or_canceled.find_each do |build| + pipeline.builds.latest.failed_or_canceled.find_each do |build| next unless build.retryable? Ci::RetryBuildService.new(project, current_user) .reprocess(build) end - pipeline.builds.skipped.find_each do |skipped| + pipeline.builds.latest.skipped.find_each do |skipped| retry_optimistic_lock(skipped) { |build| build.process } end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 4e878ec556a..1d65c76d282 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -1,6 +1,8 @@ module Groups class UpdateService < Groups::BaseService def execute + reject_parent_id! + # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level @@ -22,5 +24,11 @@ module Groups false end end + + private + + def reject_parent_id! + params.except!(:parent_id) + end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index a444c78b609..b7fe5cb168b 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -19,7 +19,7 @@ module Issues if issue.previous_changes.include?('title') || issue.previous_changes.include?('description') - todo_service.update_issue(issue, current_user) + todo_service.update_issue(issue, current_user, old_mentioned_users) end if issue.previous_changes.include?('milestone_id') diff --git a/app/services/labels/base_service.rb b/app/services/labels/base_service.rb new file mode 100644 index 00000000000..91d72a57b4e --- /dev/null +++ b/app/services/labels/base_service.rb @@ -0,0 +1,161 @@ +module Labels + class BaseService < ::BaseService + COLOR_NAME_TO_HEX = { + black: '#000000', + silver: '#C0C0C0', + gray: '#808080', + white: '#FFFFFF', + maroon: '#800000', + red: '#FF0000', + purple: '#800080', + fuchsia: '#FF00FF', + green: '#008000', + lime: '#00FF00', + olive: '#808000', + yellow: '#FFFF00', + navy: '#000080', + blue: '#0000FF', + teal: '#008080', + aqua: '#00FFFF', + orange: '#FFA500', + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + blanchedalmond: '#FFEBCD', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgreen: '#006400', + darkgrey: '#A9A9A9', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + greenyellow: '#ADFF2F', + grey: '#808080', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgray: '#D3D3D3', + lightgreen: '#90EE90', + lightgrey: '#D3D3D3', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + limegreen: '#32CD32', + linen: '#FAF0E6', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + oldlace: '#FDF5E6', + olivedrab: '#6B8E23', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + whitesmoke: '#F5F5F5', + yellowgreen: '#9ACD32', + rebeccapurple: '#663399' + }.freeze + + def convert_color_name_to_hex + color = params[:color] + color_name = color.strip.downcase + + return color if color_name.start_with?('#') + + COLOR_NAME_TO_HEX[color_name.to_sym] || color + end + end +end diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb new file mode 100644 index 00000000000..6c399c92377 --- /dev/null +++ b/app/services/labels/create_service.rb @@ -0,0 +1,25 @@ +module Labels + class CreateService < Labels::BaseService + def initialize(params = {}) + @params = params.dup.with_indifferent_access + end + + # returns the created label + def execute(target_params) + params[:color] = convert_color_name_to_hex if params[:color].present? + + project_or_group = target_params[:project] || target_params[:group] + + if project_or_group.present? + project_or_group.labels.create(params) + elsif target_params[:template] + label = Label.new(params) + label.template = true + label.save + label + else + Rails.logger.warn("target_params should contain :project or :group or :template, actual value: #{target_params}") + end + end + end +end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index cf4f7606c94..940c8b333d3 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -3,7 +3,7 @@ module Labels def initialize(current_user, project, params = {}) @current_user = current_user @project = project - @params = params.dup + @params = params.dup.with_indifferent_access end def execute(skip_authorization: false) @@ -28,7 +28,7 @@ module Labels new_label = available_labels.find_by(title: title) if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) - new_label = project.labels.create(params) + new_label = Labels::CreateService.new(params).execute(project: project) end new_label diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb new file mode 100644 index 00000000000..28dcabf9541 --- /dev/null +++ b/app/services/labels/update_service.rb @@ -0,0 +1,15 @@ +module Labels + class UpdateService < Labels::BaseService + def initialize(params = {}) + @params = params.dup.with_indifferent_access + end + + # returns the updated label + def execute(label) + params[:color] = convert_color_name_to_hex if params[:color].present? + + label.update(params) + label + end + end +end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 3cb9aae83f6..ab7fcf3b6e2 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -28,7 +28,7 @@ module MergeRequests if merge_request.previous_changes.include?('title') || merge_request.previous_changes.include?('description') - todo_service.update_merge_request(merge_request, current_user) + todo_service.update_merge_request(merge_request, current_user, old_mentioned_users) end if merge_request.previous_changes.include?('target_branch') diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb new file mode 100644 index 00000000000..a6f6320d573 --- /dev/null +++ b/app/services/note_summary.rb @@ -0,0 +1,20 @@ +class NoteSummary + attr_reader :note + attr_reader :metadata + + def initialize(noteable, project, author, body, action: nil, commit_count: nil) + @note = { noteable: noteable, project: project, author: author, note: body } + @metadata = { action: action, commit_count: commit_count }.compact + + set_commit_params if note[:noteable].is_a?(Commit) + end + + def metadata? + metadata.present? + end + + def set_commit_params + note.merge!(noteable_type: 'Commit', commit_id: note[:noteable].id) + note[:noteable] = nil + end +end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 75a4b3ed826..75fd08ea0a9 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -3,11 +3,13 @@ module Notes def execute(note) return note unless note.editable? + old_mentioned_users = note.mentioned_users.to_a + note.update_attributes(params.merge(updated_by: current_user)) note.create_new_cross_references!(current_user) if note.previous_changes.include?('note') - TodoService.new.update_note(note, current_user) + TodoService.new.update_note(note, current_user, old_mentioned_users) end note diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 44ae23fad18..940e850600f 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -38,7 +38,7 @@ class NotificationRecipientService recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) if skip_current_user + recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? recipients.uniq end @@ -47,7 +47,7 @@ class NotificationRecipientService recipients = add_labels_subscribers([], target, labels: labels) recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) + recipients.delete(current_user) unless current_user.notified_of_own_activity? recipients.uniq end @@ -88,7 +88,7 @@ class NotificationRecipientService recipients = reject_unsubscribed_users(recipients, note.noteable) recipients = reject_users_without_access(recipients, note.noteable) - recipients.delete(note.author) + recipients.delete(note.author) unless note.author.notified_of_own_activity? recipients.uniq end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index f9aa2346759..2c6f849259e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -280,8 +280,9 @@ class NotificationService recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( pipeline, - nil, # The acting user, who won't be added to recipients - action: pipeline.status).map(&:notification_email) + pipeline.user, + action: pipeline.status, + skip_current_user: false).map(&:notification_email) if recipients.any? mailer.public_send(email_template, pipeline, recipients).deliver_later diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index 781cd13b44b..a3c655493a5 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -16,5 +16,9 @@ module Search Gitlab::SearchResults.new(current_user, projects, params[:search]) end + + def scope + @scope ||= %w[issues merge_requests milestones].delete(params[:scope]) { 'projects' } + end end end diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index 4b500914cfb..9a22abae635 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -12,5 +12,9 @@ module Search params[:search], params[:repository_ref]) end + + def scope + @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' } + end end end diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb index 0b3e713e220..4f161beea4d 100644 --- a/app/services/search/snippet_service.rb +++ b/app/services/search/snippet_service.rb @@ -11,5 +11,9 @@ module Search Gitlab::SnippetSearchResults.new(snippets, params[:search]) end + + def scope + @scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' } + end end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb new file mode 100644 index 00000000000..8d46a8dab3e --- /dev/null +++ b/app/services/search_service.rb @@ -0,0 +1,63 @@ +class SearchService + include Gitlab::Allowable + + def initialize(current_user, params = {}) + @current_user = current_user + @params = params.dup + end + + def project + return @project if defined?(@project) + + @project = + if params[:project_id].present? + the_project = Project.find_by(id: params[:project_id]) + can?(current_user, :download_code, the_project) ? the_project : nil + else + nil + end + end + + def group + return @group if defined?(@group) + + @group = + if params[:group_id].present? + the_group = Group.find_by(id: params[:group_id]) + can?(current_user, :read_group, the_group) ? the_group : nil + else + nil + end + end + + def show_snippets? + return @show_snippets if defined?(@show_snippets) + + @show_snippets = params[:snippets] == 'true' + end + + delegate :scope, to: :search_service + + def search_results + @search_results ||= search_service.execute + end + + def search_objects + @search_objects ||= search_results.objects(scope, params[:page]) + end + + private + + def search_service + @search_service ||= + if project + Search::ProjectService.new(project, current_user, params) + elsif show_snippets? + Search::SnippetService.new(current_user, params) + else + Search::GlobalService.new(current_user, params) + end + end + + attr_reader :current_user, :params +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 8e02fe3741a..d3e502b66dd 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -26,7 +26,7 @@ module SystemNoteService body << new_commit_summary(new_commits).join("\n") body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count)) end # Called when the assignee of a Noteable is changed or removed @@ -46,7 +46,7 @@ module SystemNoteService def change_assignee(noteable, project, author, assignee) body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee')) end # Called when one or more labels on a Noteable are added and/or removed @@ -86,7 +86,7 @@ module SystemNoteService body << ' ' << 'label'.pluralize(labels_count) - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'label')) end # Called when the milestone of a Noteable is changed @@ -106,7 +106,7 @@ module SystemNoteService def change_milestone(noteable, project, author, milestone) body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone')) end # Called when the estimated time of a Noteable is changed @@ -132,7 +132,7 @@ module SystemNoteService "changed time estimate to #{parsed_time}" end - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) end # Called when the spent time of a Noteable is changed @@ -161,7 +161,7 @@ module SystemNoteService body = "#{action} #{parsed_time} of time spent" end - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) end # Called when the status of a Noteable is changed @@ -183,53 +183,57 @@ module SystemNoteService body = status.dup body << " via #{source.gfm_reference(project)}" if source - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'status')) end # Called when 'merge when pipeline succeeds' is executed def merge_when_pipeline_succeeds(noteable, project, author, last_commit) body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end # Called when 'merge when pipeline succeeds' is canceled def cancel_merge_when_pipeline_succeeds(noteable, project, author) body = 'canceled the automatic merge' - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end def remove_merge_request_wip(noteable, project, author) body = 'unmarked as a **Work In Progress**' - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end def add_merge_request_wip(noteable, project, author) body = 'marked as a **Work In Progress**' - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end def add_merge_request_wip_from_commit(noteable, project, author, commit) body = "marked as a **Work In Progress** from #{commit.to_reference(project)}" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end def self.resolve_all_discussions(merge_request, project, author) body = "resolved all discussions" - create_note(noteable: merge_request, project: project, author: author, note: body) + create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion')) end def discussion_continued_in_issue(discussion, project, author, issue) body = "created #{issue.to_reference} to continue this discussion" - note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) - note_attributes[:type] = note_attributes.delete(:note_type) - create_note(note_attributes) + note_params = discussion.reply_attributes.merge(project: project, author: author, note: body) + note_params[:type] = note_params.delete(:note_type) + + note = Note.create(note_params.merge(system: true)) + note.system_note_metadata = SystemNoteMetadata.new({ action: 'discussion' }) + + note end # Called when the title of a Noteable is changed @@ -253,7 +257,8 @@ module SystemNoteService marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end # Called when the confidentiality changes @@ -269,7 +274,8 @@ module SystemNoteService # Returns the created Note object def change_issue_confidentiality(issue, project, author) body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone' - create_note(noteable: issue, project: project, author: author, note: body) + + create_note(NoteSummary.new(issue, project, author, body, action: 'confidentiality')) end # Called when a branch in Noteable is changed @@ -288,7 +294,8 @@ module SystemNoteService # Returns the created Note object def change_branch(noteable, project, author, branch_type, old_branch, new_branch) body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'branch')) end # Called when a branch in Noteable is added or deleted @@ -314,7 +321,8 @@ module SystemNoteService end body = "#{verb} #{branch_type} branch `#{branch}`" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'branch')) end # Called when a branch is created from the 'new branch' button on a issue @@ -325,7 +333,8 @@ module SystemNoteService link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "created branch [`#{branch}`](#{link})" - create_note(noteable: issue, project: project, author: author, note: body) + + create_note(NoteSummary.new(issue, project, author, body, action: 'branch')) end # Called when a Mentionable references a Noteable @@ -349,23 +358,12 @@ module SystemNoteService return if cross_reference_disallowed?(noteable, mentioner) gfm_reference = mentioner.gfm_reference(noteable.project) - - note_options = { - project: noteable.project, - author: author, - note: cross_reference_note_content(gfm_reference) - } - - if noteable.is_a?(Commit) - note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) - else - note_options[:noteable] = noteable - end + body = cross_reference_note_content(gfm_reference) if noteable.is_a?(ExternalIssue) noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author) else - create_note(note_options) + create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference')) end end @@ -444,7 +442,8 @@ module SystemNoteService def change_task_status(noteable, project, author, new_task) status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE body = "marked the task **#{new_task.source}** as #{status_label}" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'task')) end # Called when noteable has been moved to another project @@ -466,7 +465,8 @@ module SystemNoteService cross_reference = noteable_ref.to_reference(project) body = "moved #{direction} #{cross_reference}" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end private @@ -482,8 +482,11 @@ module SystemNoteService end end - def create_note(args = {}) - Note.create(args.merge(system: true)) + def create_note(note_summary) + note = Note.create(note_summary.note.merge(system: true)) + note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata? + + note end def cross_reference_note_prefix diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index bf7e76ec59e..2c56cb4c680 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -19,8 +19,8 @@ class TodoService # # * mark all pending todos related to the issue for the current user as done # - def update_issue(issue, current_user) - update_issuable(issue, current_user) + def update_issue(issue, current_user, skip_users = []) + update_issuable(issue, current_user, skip_users) end # When close an issue we should: @@ -60,8 +60,8 @@ class TodoService # # * create a todo for each mentioned user on merge request # - def update_merge_request(merge_request, current_user) - update_issuable(merge_request, current_user) + def update_merge_request(merge_request, current_user, skip_users = []) + update_issuable(merge_request, current_user, skip_users) end # When close a merge request we should: @@ -123,7 +123,7 @@ class TodoService mark_pending_todos_as_done(merge_request, merge_request.author) mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end - + # When a merge request could not be automatically merged due to its unmergeable state we should: # # * create a todo for a merge_user @@ -131,7 +131,7 @@ class TodoService def merge_request_became_unmergeable(merge_request) create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end - + # When create a note we should: # # * mark all pending todos related to the noteable for the note author as done @@ -146,8 +146,8 @@ class TodoService # * mark all pending todos related to the noteable for the current user as done # * create a todo for each new user mentioned on note # - def update_note(note, current_user) - handle_note(note, current_user) + def update_note(note, current_user, skip_users = []) + handle_note(note, current_user, skip_users) end # When an emoji is awarded we should: @@ -223,11 +223,11 @@ class TodoService create_mention_todos(issuable.project, issuable, author) end - def update_issuable(issuable, author) + def update_issuable(issuable, author, skip_users = []) # Skip toggling a task list item in a description return if toggling_tasks?(issuable) - create_mention_todos(issuable.project, issuable, author) + create_mention_todos(issuable.project, issuable, author, nil, skip_users) end def destroy_issuable(issuable, user) @@ -239,7 +239,7 @@ class TodoService issuable.tasks? && issuable.updated_tasks.any? end - def handle_note(note, author) + def handle_note(note, author, skip_users = []) # Skip system notes, and notes on project snippet return if note.system? || note.for_snippet? @@ -247,7 +247,7 @@ class TodoService target = note.noteable mark_pending_todos_as_done(target, author) - create_mention_todos(project, target, author, note) + create_mention_todos(project, target, author, note, skip_users) end def create_assignment_todo(issuable, author) @@ -257,14 +257,14 @@ class TodoService end end - def create_mention_todos(project, target, author, note = nil) + def create_mention_todos(project, target, author, note = nil, skip_users = []) # Create Todos for directly addressed users - directly_addressed_users = filter_directly_addressed_users(project, note || target, author) + directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users) attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note) create_todos(directly_addressed_users, attributes) # Create Todos for mentioned users - mentioned_users = filter_mentioned_users(project, note || target, author) + mentioned_users = filter_mentioned_users(project, note || target, author, skip_users) attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note) create_todos(mentioned_users, attributes) end @@ -307,13 +307,13 @@ class TodoService reject_users_without_access(users, project, target).uniq end - def filter_mentioned_users(project, target, author) - mentioned_users = target.mentioned_users(author) + def filter_mentioned_users(project, target, author, skip_users = []) + mentioned_users = target.mentioned_users(author) - skip_users filter_todo_users(mentioned_users, project, target) end - def filter_directly_addressed_users(project, target, author) - directly_addressed_users = target.directly_addressed_users(author) + def filter_directly_addressed_users(project, target, author, skip_users = []) + directly_addressed_users = target.directly_addressed_users(author) - skip_users filter_todo_users(directly_addressed_users, project, target) end diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb index f4f0b80f30a..193fcd85896 100644 --- a/app/services/users/create_service.rb +++ b/app/services/users/create_service.rb @@ -94,7 +94,7 @@ module Users def build_user_params if current_user&.is_admin? user_params = params.slice(*admin_create_params) - user_params[:created_by_id] = current_user.id + user_params[:created_by_id] = current_user&.id if params[:reset_password] user_params.merge!(force_random_password: true, password_expires_at: nil) diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index c1a9f8d6ddd..596f367a00d 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -1,15 +1,16 @@ .js-projects-list-holder - if @projects.any? - %ul.projects-list.content-list + %ul.projects-list.content-list.admin-projects - @projects.each_with_index do |project| - %li.project-row + %li.project-row{ class: ('no-description' if project.description.blank?) } .controls - - if project.archived - %span.label.label-warning archived - %span.badge - = storage_counter(project.statistics.storage_size) = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .stats + %span.badge + = storage_counter(project.statistics.storage_size) + - if project.archived + %span.label.label-warning archived .title = link_to [:admin, project.namespace.becomes(Namespace), project] do .dash-project-avatar @@ -20,7 +21,7 @@ - if project.namespace = project.namespace.human_name \/ - %span.project-name.filter-title + %span.project-name = project.name - if project.description.present? diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 600ee63a5c0..4679b9549d1 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,7 +1,9 @@ = content_for :flash_message do = render 'shared/project_limit' -.top-area - %ul.nav-links +.top-area.scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.scrolling-tabs = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do Your projects diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 60c84a26420..2129920afd2 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,5 +1,5 @@ - header_title "Milestones", dashboard_milestones_path = render 'shared/milestones/top', milestone: @milestone -= render 'shared/milestones/summary', milestone: @milestone = render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true += render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 51 diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index eef794dbd51..596499230f9 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -4,7 +4,9 @@ - page_title "Projects" - header_title "Projects", dashboard_projects_path -.user-callout{ 'callout-svg' => custom_icon('icon_customization') } +- unless show_user_callout? + = render 'shared/user_callout' + - if @projects.any? || params[:name] = render 'dashboard/projects_head' diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 6f5d4bf2a2f..2d78c55211e 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -8,7 +8,7 @@ .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } } .discussion-header .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" } - if expanded = icon("chevron-up") - else diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index e66a8e0a3b3..8e83b2002b2 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -4,5 +4,5 @@ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? = render 'shared/milestones/top', milestone: @milestone, group: @group -= render 'shared/milestones/summary', milestone: @milestone = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true += render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102 diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7ddee0e5244..23abf6897d4 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -11,9 +11,13 @@ = render 'layouts/nav/dashboard' - else = render 'layouts/nav/explore' - %button.navbar-toggle{ type: 'button' } - %span.sr-only Toggle navigation - = icon('ellipsis-v') + + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do + = brand_header_logo + + .title-container + %h1.title{ class: ('initializing' if @has_group_title) }= title .navbar-collapse.collapse %ul.nav.navbar-nav @@ -31,11 +35,6 @@ %li = link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - %li - = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('bell fw') - %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } - = todos_count_format(todos_pending_count) - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do @@ -45,6 +44,21 @@ = link_to sherlock_transactions_path, title: 'Sherlock Transactions', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('tachometer fw') + %li + = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('hashtag fw') + %span.badge.issues-count + = number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) + %li + = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = custom_icon('mr_bold') + %span.badge.merge-requests-count + = number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) + %li + = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('check-circle fw') + %span.badge.todos-count + = todos_count_format(todos_pending_count) %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" @@ -63,11 +77,9 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do - = brand_header_logo - - %h1.title{ class: ('initializing' if @has_group_title) }= title + %button.navbar-toggle{ type: 'button' } + %span.sr-only Toggle navigation + = icon('ellipsis-v') = yield :header_content diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml index b28fea35ad5..76440926a2b 100644 --- a/app/views/notify/project_was_exported_email.html.haml +++ b/app/views/notify/project_was_exported_email.html.haml @@ -2,7 +2,7 @@ Project #{@project.name} was exported successfully. %p The project export can be downloaded from: - = link_to download_export_namespace_project_url(@project.namespace, @project) do + = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do = @project.name_with_namespace + " export" %p The download link will expire in 24 hours. diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 8a994f6d600..5ce2220c907 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -75,12 +75,12 @@ .provider-btn-image = provider_image_tag(provider) - if auth_active?(provider) - - if provider.to_s == 'saml' - %a.provider-btn - Active - - else + - if unlink_allowed?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect + - else + %a.provider-btn + Active - else = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do Connect diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 5c5e5940365..51c4e8e5a73 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -34,6 +34,11 @@ .clearfix + = form_for @user, url: profile_notifications_path, method: :put do |f| + %label{ for: 'user_notified_of_own_activity' } + = f.check_box :notified_of_own_activity + %span Receive notifications about your own activity + %hr %h5 Groups (#{@group_notifications.count}) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 79a0dc1b959..0fd19780570 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,6 @@ - empty_repo = @project.empty_repo? .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } - %div{ class: container_class } + .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') %h1.project-title diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index edf55d59f28..de8c173f26f 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -3,7 +3,7 @@ .top-block.row-content-block.clearfix .pull-right = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), - class: 'btn btn-default download' do + rel: 'nofollow', download: '', class: 'btn btn-default download' do = icon('download') Download artifacts archive diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/_notebook.html.haml new file mode 100644 index 00000000000..ab1cf933944 --- /dev/null +++ b/app/views/projects/blob/_notebook.html.haml @@ -0,0 +1,5 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('notebook_viewer') + +.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_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 0bca6a786cb..5a4eaf92b16 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -7,12 +7,12 @@ data: { container: "body", placement: "bottom" } } {{ list.title }} .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } - %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' } + %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ 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"', + "v-if" => 'list.type !== "closed"', "aria-label" => "Add an issue", "title" => "Add an issue", data: { placement: "top", container: "body" } } diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index 4a4dd84d5d2..4a0b2110601 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -3,7 +3,7 @@ = icon("spinner spin") - if can? current_user, :create_issue, @project %board-new-issue{ ":list" => "list", - "v-if" => 'list.type !== "done" && showIssueForm' } + "v-if" => 'list.type !== "closed" && showIssueForm' } %ul.board-list{ "ref" => "list", "v-show" => "!loading", ":data-board" => "list.id", diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index b597c7f7a12..6f45d5b0689 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -33,7 +33,7 @@ = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do Keep - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do Download - if @build.artifacts_metadata? diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 09286a1b3c6..aeed293a724 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -94,7 +94,7 @@ %td .pull-right - if can?(current_user, :read_build, build) && build.artifacts? - = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do + = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = icon('download') - if can?(current_user, :update_build, build) - if build.active? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 6ab9a80e083..4b1ff75541a 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -24,7 +24,7 @@ .visible-xs-inline = render_commit_status(commit, ref: ref) - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ... - if commit.description? %pre.commit-row-description.js-toggle-content diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 8e24e28765f..fd4f3c8d3cc 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,7 +1,7 @@ .js-toggle-container .commit-stat-summary Showing - = link_to '#', class: 'js-toggle-button' do + %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" } %strong= pluralize(diff_files.size, "changed file") with %strong.cgreen #{diff_files.sum(&:added_lines)} additions diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 82e0d0025ec..b78de092a60 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -163,7 +163,7 @@ - if @project.export_project_path = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), - method: :get, class: "btn btn-default" + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" = link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" - else diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 6682a85ffa6..881ee9fd596 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -52,8 +52,10 @@ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - .merge-request-tabs-container - %ul.merge-request-tabs.nav-links.no-top.no-bottom + .merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.merge-request-tabs.nav-links.scrolling-tabs %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/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index c94c7944c0b..e5ec151a61d 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -37,7 +37,7 @@ = check_box_tag :should_remove_source_branch Remove source branch .accept-control - = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + %button.modify-merge-commit-link.js-toggle-button{ type: "button" } = icon('edit') Modify commit message .js-toggle-content.hide.prepend-top-default diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index d16f49bd33a..f612b5c7d6b 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -36,6 +36,9 @@ = 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 + %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } + = icon('angle-double-left') + .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) } %h2.title = markdown_field(@milestone, :title) @@ -53,5 +56,5 @@ .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/sidebar', milestone: @milestone, project: @project, affix_offset: 153 diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 34a1214a350..09ac1fd6794 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -76,7 +76,7 @@ Gitea %div - if git_import_enabled? - = link_to "#", class: 'btn js-toggle-button import_git' do + %button.btn.js-toggle-button.import_git{ type: "button" } = icon('git', text: 'Repo by URL') .import_gitlab_project - if gitlab_project_import_enabled? diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index b02fef638ff..bc57f7f1c46 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -10,13 +10,13 @@ Pipelines - if project_nav_tab? :builds - = nav_link(path: 'builds#index', controller: :builds) do + = nav_link(controller: :builds) do = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span Jobs - if project_nav_tab? :environments - = nav_link(path: 'environments#index', controller: :environments) do + = nav_link(controller: :environments) do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml index a9e27df5a87..5af0cc7a2f3 100644 --- a/app/views/projects/protected_branches/_dropdown.html.haml +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -10,6 +10,6 @@ %ul.dropdown-footer-list %li - = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do + %button{ class: "create-new-protected-branch-button js-create-new-protected-branch", title: "New Protected Branch" } Create wildcard %code diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index de1229d58aa..edfe6da1816 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,7 +13,7 @@ = render "home_panel" - if current_user && can?(current_user, :download_code, @project) - %nav.project-stats{ class: container_class } + %nav.project-stats.limit-container-width{ class: container_class } %ul.nav %li = link_to project_files_path(@project) do @@ -74,11 +74,11 @@ Set up auto deploy - if @repository.commit - %div{ class: container_class } + .limit-container-width{ class: container_class } .project-last-commit = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project -%div{ class: container_class } +.limit-container-width{ class: container_class } - if @project.archived? .text-warning.center.prepend-top-20 %p diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 5afb95ac430..059a0d1ac78 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,71 +1,74 @@ -%ul.nav-links.search-filter - - if @project - %li{ class: active_when(@scope == 'blobs') } - = link_to search_filter_path(scope: 'blobs') do - Code - %span.badge - = @search_results.blobs_count - %li{ class: active_when(@scope == 'issues') } - = link_to search_filter_path(scope: 'issues') do - Issues - %span.badge - = @search_results.issues_count - %li{ class: active_when(@scope == 'merge_requests') } - = link_to search_filter_path(scope: 'merge_requests') do - Merge requests - %span.badge - = @search_results.merge_requests_count - %li{ class: active_when(@scope == 'milestones') } - = link_to search_filter_path(scope: 'milestones') do - Milestones - %span.badge - = @search_results.milestones_count - %li{ class: active_when(@scope == 'notes') } - = link_to search_filter_path(scope: 'notes') do - Comments - %span.badge - = @search_results.notes_count - %li{ class: active_when(@scope == 'wiki_blobs') } - = link_to search_filter_path(scope: 'wiki_blobs') do - Wiki - %span.badge - = @search_results.wiki_blobs_count - %li{ class: active_when(@scope == 'commits') } - = link_to search_filter_path(scope: 'commits') do - Commits - %span.badge - = @search_results.commits_count +.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.search-filter.scrolling-tabs + - if @project + %li{ class: active_when(@scope == 'blobs') } + = link_to search_filter_path(scope: 'blobs') do + Code + %span.badge + = @search_results.blobs_count + %li{ class: active_when(@scope == 'issues') } + = link_to search_filter_path(scope: 'issues') do + Issues + %span.badge + = @search_results.issues_count + %li{ class: active_when(@scope == 'merge_requests') } + = link_to search_filter_path(scope: 'merge_requests') do + Merge requests + %span.badge + = @search_results.merge_requests_count + %li{ class: active_when(@scope == 'milestones') } + = link_to search_filter_path(scope: 'milestones') do + Milestones + %span.badge + = @search_results.milestones_count + %li{ class: active_when(@scope == 'notes') } + = link_to search_filter_path(scope: 'notes') do + Comments + %span.badge + = @search_results.notes_count + %li{ class: active_when(@scope == 'wiki_blobs') } + = link_to search_filter_path(scope: 'wiki_blobs') do + Wiki + %span.badge + = @search_results.wiki_blobs_count + %li{ class: active_when(@scope == 'commits') } + = link_to search_filter_path(scope: 'commits') do + Commits + %span.badge + = @search_results.commits_count - - elsif @show_snippets - %li{ class: active_when(@scope == 'snippet_blobs') } - = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - Snippet Contents - %span.badge - = @search_results.snippet_blobs_count - %li{ class: active_when(@scope == 'snippet_titles') } - = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - Titles and Filenames - %span.badge - = @search_results.snippet_titles_count + - elsif @show_snippets + %li{ class: active_when(@scope == 'snippet_blobs') } + = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do + Snippet Contents + %span.badge + = @search_results.snippet_blobs_count + %li{ class: active_when(@scope == 'snippet_titles') } + = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do + Titles and Filenames + %span.badge + = @search_results.snippet_titles_count - - else - %li{ class: active_when(@scope == 'projects') } - = link_to search_filter_path(scope: 'projects') do - Projects - %span.badge - = @search_results.projects_count - %li{ class: active_when(@scope == 'issues') } - = link_to search_filter_path(scope: 'issues') do - Issues - %span.badge - = @search_results.issues_count - %li{ class: active_when(@scope == 'merge_requests') } - = link_to search_filter_path(scope: 'merge_requests') do - Merge requests - %span.badge - = @search_results.merge_requests_count - %li{ class: active_when(@scope == 'milestones') } - = link_to search_filter_path(scope: 'milestones') do - Milestones - %span.badge - = @search_results.milestones_count + - else + %li{ class: active_when(@scope == 'projects') } + = link_to search_filter_path(scope: 'projects') do + Projects + %span.badge + = @search_results.projects_count + %li{ class: active_when(@scope == 'issues') } + = link_to search_filter_path(scope: 'issues') do + Issues + %span.badge + = @search_results.issues_count + %li{ class: active_when(@scope == 'merge_requests') } + = link_to search_filter_path(scope: 'merge_requests') do + Merge requests + %span.badge + = @search_results.merge_requests_count + %li{ class: active_when(@scope == 'milestones') } + = link_to search_filter_path(scope: 'milestones') do + Milestones + %span.badge + = @search_results.milestones_count diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index c2d9ac87b20..8869d510aef 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -1,4 +1,6 @@ -- parent = Group.find_by(id: params[:parent_id] || @group.parent_id) +- parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id) +- group_path = root_url +- group_path << parent.full_path + '/' if parent - if @group.persisted? .form-group = f.label :name, class: 'control-label' do @@ -11,7 +13,7 @@ Group path .col-sm-10 .input-group.gl-field-error-anchor - .input-group-addon + .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } %span>= root_url - if parent %strong= parent.full_path + '/' diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml new file mode 100644 index 00000000000..8f1293adcb1 --- /dev/null +++ b/app/views/shared/_user_callout.html.haml @@ -0,0 +1,14 @@ +.user-callout + .bordered-box.landing.content-block + %button.btn.btn-default.close.js-close-callout{ type: 'button', + 'aria-label' => 'Dismiss customize experience box' } + = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') + .row + .col-sm-3.col-xs-12.svg-container + = custom_icon('icon_customization') + .col-sm-8.col-xs-12.inner-content + %h4 + Customize your experience + %p + Change syntax themes, default project pages, and more in preferences. + = link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout' diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg new file mode 100644 index 00000000000..2daa55a8652 --- /dev/null +++ b/app/views/shared/icons/_mr_bold.svg @@ -0,0 +1 @@ +<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg> diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b58640c3ef0..330fa8a5b10 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,7 @@ %button.btn.btn-link = icon('search') %span - Keep typing and press Enter + Press Enter or click to search %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 884bd3ca9ca..92d2d93a732 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -13,15 +13,12 @@ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } - %span.js-issuable-todo-text - - if todo - Mark done - - else - Add todo - = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true') + = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| + - if current_user + .block.todo.hide-expanded + = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true .block.assignee .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } - if issuable.assignee @@ -121,7 +118,7 @@ - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } - = icon('tags', class: 'hidden', 'aria-hidden': 'true') + = icon('tags', 'aria-hidden': 'true') %span = selected_labels.size .title.hide-collapsed diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml new file mode 100644 index 00000000000..574e2958ae8 --- /dev/null +++ b/app/views/shared/issuable/_sidebar_todo.html.haml @@ -0,0 +1,15 @@ +- is_collapsed = local_assigns.fetch(:is_collapsed, false) +- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : 'Mark done' +- todo_content = is_collapsed ? icon('plus-square') : 'Add todo' + +%button.issuable-todo-btn.js-issuable-todo{ type: 'button', + class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'), + title: (todo.nil? ? 'Add todo' : 'Mark done'), + 'aria-label' => (todo.nil? ? 'Add todo' : 'Mark done'), + data: issuable_todo_button_data(issuable, todo, is_collapsed) } + %span.issuable-todo-inner.js-issuable-todo-inner< + - if todo + = mark_content + - else + = todo_content + = icon('spin spinner', 'aria-hidden': 'true') diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 8e721c9c8dd..a5aa768b1b2 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -31,7 +31,7 @@ Joined #{time_ago_with_tooltip(member.created_at)} - if member.expires? ยท - %span{ class: ('text-warning' if member.expires_soon?) } + %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else @@ -47,7 +47,7 @@ - current_resource = @project || @group .controls.member-controls - if show_controls && member.source == current_resource - - if user != current_user + - if user != current_user && can_admin_member = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml new file mode 100644 index 00000000000..2810f1377b2 --- /dev/null +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -0,0 +1,131 @@ +- affix_offset = local_assigns.fetch(:affix_offset, "102") +- project = local_assigns[:project] + +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } + .issuable-sidebar.milestone-sidebar + .block.milestone-progress.issuable-sidebar-header + %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } + = sidebar_gutter_toggle_icon + + .sidebar-collapsed-icon + %span== #{milestone.percent_complete(current_user)}% + = milestone_progress_bar(milestone) + .title.hide-collapsed + %strong.bold== #{milestone.percent_complete(current_user)}% + %span.hide-collapsed + complete + .value.hide-collapsed + = milestone_progress_bar(milestone) + + .block.start_date.hide-collapsed + .title + Start date + - if @project && can?(current_user, :admin_milestone, @project) + = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right' + .value + %span.value-content + - if milestone.start_date + %span.bold= milestone.start_date.to_s(:medium) + - else + %span.no-value No start date + + .block.due_date + .sidebar-collapsed-icon + = icon('calendar', 'aria-hidden': 'true') + %span.collapsed-milestone-date + - if milestone.start_date && milestone.due_date + - if milestone.start_date.year == milestone.due_date.year + .milestone-date= milestone.start_date.strftime('%b %-d') + - else + .milestone-date= milestone.start_date.strftime('%b %-d %Y') + .date-separator - + .due_date= milestone.due_date.strftime('%b %-d %Y') + - elsif milestone.start_date + From + .milestone-date= milestone.start_date.strftime('%b %-d %Y') + - elsif milestone.due_date + Until + .milestone-date= milestone.due_date.strftime('%b %-d %Y') + - else + None + .title.hide-collapsed + Due date + - if @project && can?(current_user, :admin_milestone, @project) + = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right' + .value.hide-collapsed + %span.value-content + - if milestone.due_date + %span.bold= milestone.due_date.to_s(:medium) + - else + %span.no-value No due date + - remaining_days = milestone_remaining_days(milestone) + - if remaining_days.present? + = surround '(', ')' do + %span.remaining-days= remaining_days + + - if !project || can?(current_user, :read_issue, project) + .block + .sidebar-collapsed-icon + %strong + = icon('hashtag', 'aria-hidden': 'true') + %span= milestone.issues_visible_to_user(current_user).count + .title.hide-collapsed + Issues + %span.badge= milestone.issues_visible_to_user(current_user).count + - if project && can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do + New issue + .value.hide-collapsed.bold + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :issues) do + Open: + = milestone.issues_visible_to_user(current_user).opened.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :issues, state: 'closed') do + Closed: + = milestone.issues_visible_to_user(current_user).closed.count + + .block + .sidebar-collapsed-icon + %strong + = icon('exclamation', 'aria-hidden': 'true') + %span= milestone.issues_visible_to_user(current_user).count + .title.hide-collapsed + Merge requests + %span.badge= milestone.merge_requests.count + .value.hide-collapsed.bold + - if !project || can?(current_user, :read_merge_request, project) + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do + Open: + = milestone.merge_requests.opened.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do + Closed: + = milestone.merge_requests.closed.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do + Merged: + = milestone.merge_requests.merged.count + - else + %span.milestone-stat + Open: + = milestone.merge_requests.opened.count + %span.milestone-stat + Closed: + = milestone.merge_requests.closed.count + %span.milestone-stat + Merged: + = milestone.merge_requests.merged.count + + - milestone_ref = milestone.try(:to_reference, full: true) + - if milestone_ref.present? + .block.reference + .sidebar-collapsed-icon.dont-change-state + = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left") + .cross-project-reference.hide-collapsed + %span + Reference: + %cite{ title: milestone_ref } + = milestone_ref + = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left") diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml deleted file mode 100644 index 78079f633d5..00000000000 --- a/app/views/shared/milestones/_summary.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -- project = local_assigns[:project] - -.context.prepend-top-default - .milestone-summary - %h4 Progress - - .milestone-stats-and-buttons - .milestone-stats - - if !project || can?(current_user, :read_issue, project) - %span.milestone-stat.with-drilldown - %strong= milestone.issues_visible_to_user(current_user).size - issues: - %span.milestone-stat - %strong= milestone.issues_visible_to_user(current_user).opened.size - open and - %strong= milestone.issues_visible_to_user(current_user).closed.size - closed - %span.milestone-stat.with-drilldown - %strong= milestone.merge_requests.size - merge requests: - %span.milestone-stat - %strong= milestone.merge_requests.opened.size - open and - %strong= milestone.merge_requests.merged.size - merged - %span.milestone-stat - %strong== #{milestone.percent_complete(current_user)}% - complete - - remaining_days = milestone_remaining_days(milestone) - - if remaining_days.present? - %span.milestone-stat - %span.remaining-days= remaining_days - - .milestone-progress-buttons - %span.tab-issues-buttons - - if project - - if can?(current_user, :create_issue, project) - = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do - New Issue - - if can?(current_user, :read_issue, project) - = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" - %span.tab-merge-requests-buttons.hidden - = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn" - - = milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index a0e9ec46220..9a4502873ef 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,26 +1,29 @@ -%ul.nav-links.no-top.no-bottom - - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Issues - %span.badge= milestone.issues_visible_to_user(current_user).size +.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.scrolling-tabs + - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Issues + %span.badge= milestone.issues_visible_to_user(current_user).size + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size + - else + %li.active + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= milestone.merge_requests.size - - else - %li.active - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= milestone.merge_requests.size - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= milestone.participants.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab' do - Labels - %span.badge= milestone.labels.count + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= milestone.participants.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab' do + Labels + %span.badge= milestone.labels.count - show_project_name = local_assigns.fetch(:show_project_name, false) - show_full_project_name = local_assigns.fetch(:show_full_project_name, false) diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 497446c1ef3..2562f085338 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -3,6 +3,9 @@ - group = local_assigns[:group] .detail-page-header + %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } + = icon('angle-double-left') + .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" } - if milestone.closed? Closed diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 059aeebaf34..761f0b606b5 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -24,7 +24,7 @@ - if project.namespace && !skip_namespace = project.namespace.human_name \/ - %span.project-name.filter-title + %span.project-name = project.name - if show_last_commit_as_description diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 4afd31f788b..d1e88274878 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -18,9 +18,9 @@ = event_action_name(event) %strong - if event.note? - = link_to event.note_target.to_reference, event_note_target_path(event) + = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title - elsif event.target - = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target] + = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title at %strong diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 601187455b3..969ea7ab9e6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -79,26 +79,29 @@ %p.profile-user-bio = @user.bio - %ul.nav-links.center.user-profile-nav - %li.js-activity-tab - = 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 - Groups - %li.js-contributed-tab - = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do - Contributed projects - %li.js-projects-tab - = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do - Personal projects - %li.js-snippets-tab - = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do - Snippets + .scrolling-tabs-container + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.center.user-profile-nav.scrolling-tabs + %li.js-activity-tab + = 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 + Groups + %li.js-contributed-tab + = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do + Contributed projects + %li.js-projects-tab + = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do + Personal projects + %li.js-snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } - - if @user == current_user - .user-callout{ 'callout-svg' => custom_icon('icon_customization') } + - if @user == current_user && !show_user_callout? + = render 'shared/user_callout' .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 2cd87895c55..015a41b6e82 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -3,20 +3,16 @@ class PostReceive include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) - if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) } - repo_path.gsub!(repository_storage[1]['path'].to_s, "") - else - log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"") - end + repo_relative_path = Gitlab::RepoPath.strip_storage_path(repo_path) changes = Base64.decode64(changes) unless changes.include?(' ') # Use Sidekiq.logger so arguments can be correlated with execution # time and thread ID's. Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS'] - post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) + post_received = Gitlab::GitPostReceive.new(repo_relative_path, identifier, changes) if post_received.project.nil? - log("Triggered hook for non-existing project with full path \"#{repo_path}\"") + log("Triggered hook for non-existing project with full path \"#{repo_relative_path}\"") return false end @@ -25,7 +21,7 @@ class PostReceive elsif post_received.regular_project? process_project_changes(post_received) else - log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"") + log("Triggered hook for unidentifiable repository type with full path \"#{repo_relative_path}\"") false end end |