summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js8
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/spread_string.js50
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js2
-rw-r--r--app/assets/javascripts/blob/notebook/index.js85
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js3
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board_card.js4
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js7
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js11
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js19
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.js31
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js12
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js16
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js20
-rw-r--r--app/assets/javascripts/group_name.js42
-rw-r--r--app/assets/javascripts/groups_select.js78
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/layout_nav.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js16
-rw-r--r--app/assets/javascripts/lib/utils/poll.js55
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js33
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js8
-rw-r--r--app/assets/javascripts/right_sidebar.js49
-rw-r--r--app/assets/javascripts/user_callout.js43
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js1
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js6
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js1
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss78
-rw-r--r--app/assets/stylesheets/framework/header.scss105
-rw-r--r--app/assets/stylesheets/framework/mixins.scss12
-rw-r--r--app/assets/stylesheets/framework/nav.scss45
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss7
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss15
-rw-r--r--app/assets/stylesheets/pages/environments.scss11
-rw-r--r--app/assets/stylesheets/pages/groups.scss17
-rw-r--r--app/assets/stylesheets/pages/issuable.scss31
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/milestone.scss82
-rw-r--r--app/assets/stylesheets/pages/notes.scss30
-rw-r--r--app/assets/stylesheets/pages/projects.scss33
-rw-r--r--app/assets/stylesheets/pages/todos.scss19
-rw-r--r--app/controllers/admin/application_settings_controller.rb9
-rw-r--r--app/controllers/admin/background_jobs_controller.rb4
-rw-r--r--app/controllers/admin/labels_controller.rb9
-rw-r--r--app/controllers/groups/labels_controller.rb6
-rw-r--r--app/controllers/import/base_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb13
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb4
-rw-r--r--app/controllers/projects/labels_controller.rb6
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb52
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/search_controller.rb40
-rw-r--r--app/finders/group_finder.rb17
-rw-r--r--app/finders/labels_finder.rb13
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb17
-rw-r--r--app/helpers/milestones_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb3
-rw-r--r--app/helpers/sidekiq_helper.rb6
-rw-r--r--app/models/blob.rb6
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/commit_status.rb4
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/namespace.rb18
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb6
-rw-r--r--app/models/project_team.rb3
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/system_note_metadata.rb11
-rw-r--r--app/models/user.rb7
-rw-r--r--app/serializers/build_entity.rb7
-rw-r--r--app/serializers/build_serializer.rb8
-rw-r--r--app/serializers/environment_entity.rb7
-rw-r--r--app/serializers/pipeline_entity.rb11
-rw-r--r--app/serializers/pipeline_serializer.rb7
-rw-r--r--app/serializers/status_entity.rb2
-rw-r--r--app/services/boards/create_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/ci/retry_pipeline_service.rb4
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/labels/base_service.rb161
-rw-r--r--app/services/labels/create_service.rb25
-rw-r--r--app/services/labels/find_or_create_service.rb4
-rw-r--r--app/services/labels/update_service.rb15
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/note_summary.rb20
-rw-r--r--app/services/notes/update_service.rb4
-rw-r--r--app/services/notification_recipient_service.rb6
-rw-r--r--app/services/notification_service.rb5
-rw-r--r--app/services/search/global_service.rb4
-rw-r--r--app/services/search/project_service.rb4
-rw-r--r--app/services/search/snippet_service.rb4
-rw-r--r--app/services/search_service.rb63
-rw-r--r--app/services/system_note_service.rb79
-rw-r--r--app/services/todo_service.rb38
-rw-r--r--app/services/users/create_service.rb2
-rw-r--r--app/views/admin/projects/_projects.html.haml15
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/milestones/show.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml4
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml38
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml8
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blob/_notebook.html.haml5
-rw-r--r--app/views/projects/boards/components/_board.html.haml4
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml4
-rw-r--r--app/views/projects/protected_branches/_dropdown.html.haml2
-rw-r--r--app/views/projects/show.html.haml6
-rw-r--r--app/views/search/_category.html.haml141
-rw-r--r--app/views/shared/_group_form.html.haml6
-rw-r--r--app/views/shared/_user_callout.html.haml14
-rw-r--r--app/views/shared/icons/_mr_bold.svg1
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml15
-rw-r--r--app/views/shared/members/_member.html.haml4
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml131
-rw-r--r--app/views/shared/milestones/_summary.html.haml45
-rw-r--r--app/views/shared/milestones/_tabs.html.haml47
-rw-r--r--app/views/shared/milestones/_top.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml4
-rw-r--r--app/views/users/show.html.haml39
-rw-r--r--app/workers/post_receive.rb12
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