summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js10
-rw-r--r--app/assets/javascripts/awards_handler.js32
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue10
-rw-r--r--app/assets/javascripts/diffs/store/utils.js5
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue6
-rw-r--r--app/assets/javascripts/header.js55
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue4
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue15
-rw-r--r--app/assets/javascripts/jobs/store/getters.js13
-rw-r--r--app/assets/javascripts/notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js12
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js4
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js26
-rw-r--r--app/assets/javascripts/pages/users/user_overview_block.js42
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js84
-rw-r--r--app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js21
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue33
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue241
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss12
-rw-r--r--app/assets/stylesheets/framework/header.scss36
-rw-r--r--app/assets/stylesheets/framework/mixins.scss66
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss1
-rw-r--r--app/assets/stylesheets/pages/notes.scss54
-rw-r--r--app/assets/stylesheets/pages/profile.scss10
-rw-r--r--app/assets/stylesheets/pages/projects.scss8
-rw-r--r--app/assets/stylesheets/performance_bar.scss3
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/controllers/concerns/labels_as_hash.rb28
-rw-r--r--app/controllers/groups/labels_controller.rb3
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb7
-rw-r--r--app/controllers/projects/labels_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb7
-rw-r--r--app/controllers/users_controller.rb13
-rw-r--r--app/finders/group_labels_finder.rb16
-rw-r--r--app/finders/labels_finder.rb13
-rw-r--r--app/finders/merge_requests_finder.rb4
-rw-r--r--app/finders/user_recent_events_finder.rb2
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/clusters_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb20
-rw-r--r--app/helpers/repository_languages_helper.rb3
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/concerns/subscribable.rb8
-rw-r--r--app/models/instance_configuration.rb4
-rw-r--r--app/models/label.rb9
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/serializers/commit_entity.rb21
-rw-r--r--app/serializers/detailed_status_entity.rb8
-rw-r--r--app/serializers/diffs_entity.rb4
-rw-r--r--app/services/projects/autocomplete_service.rb29
-rw-r--r--app/services/quick_actions/interpret_service.rb19
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml183
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml4
-rw-r--r--app/views/ci/runner/_how_to_setup_shared_runner.html.haml3
-rw-r--r--app/views/ci/runner/_how_to_setup_specific_runner.html.haml26
-rw-r--r--app/views/groups/labels/index.html.haml21
-rw-r--r--app/views/groups/runners/_group_runners.html.haml4
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml7
-rw-r--r--app/views/layouts/header/_default.html.haml3
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml17
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml15
-rw-r--r--app/views/projects/clusters/user/_form.html.haml17
-rw-r--r--app/views/projects/clusters/user/_show.html.haml15
-rw-r--r--app/views/projects/environments/show.html.haml9
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml18
-rw-r--r--app/views/projects/jobs/_empty_states.html.haml9
-rw-r--r--app/views/projects/jobs/show.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml23
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml30
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml10
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/labels/_nav.html.haml20
-rw-r--r--app/views/shared/labels/_sort_dropdown.html.haml2
-rw-r--r--app/views/users/_overview.html.haml32
-rw-r--r--app/views/users/show.html.haml38
89 files changed, 1154 insertions, 448 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cd800d75f7a..ecc2440c7e6 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -22,6 +22,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
+ userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
@@ -266,6 +267,15 @@ const Api = {
});
},
+ postUserStatus({ emoji, message }) {
+ const url = Api.buildUrl(this.userStatusPath);
+
+ return axios.put(url, {
+ emoji,
+ message,
+ });
+ },
+
templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 5b0c4285339..cace8bb9dba 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -42,10 +42,11 @@ export class AwardsHandler {
}
bindEvents() {
+ const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
- $(document),
+ $parentEl,
'mouseenter focus',
this.toggleButtonSelector,
'mouseenter focus',
@@ -58,7 +59,7 @@ export class AwardsHandler {
}
},
);
- this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
+ this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
@@ -76,7 +77,7 @@ export class AwardsHandler {
});
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
- this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
+ this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
@@ -168,7 +169,8 @@ export class AwardsHandler {
</div>
`;
- document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
+ const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
+ targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories();
this.setupSearch();
@@ -250,6 +252,12 @@ export class AwardsHandler {
}
positionMenu($menu, $addBtn) {
+ if (this.targetContainerEl) {
+ return $menu.css({
+ top: `${$addBtn.outerHeight()}px`,
+ });
+ }
+
const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body
@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
users.unshift('You');
- return awardBlock
- .attr('title', this.toSentence(users))
- .tooltip('_fixTitle');
+ return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
- awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
- Emoji => {
- const awardsHandler = new AwardsHandler(Emoji);
- awardsHandler.bindEvents();
- return awardsHandler;
- },
- );
+ awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
+ const awardsHandler = new AwardsHandler(Emoji);
+ awardsHandler.bindEvents();
+ return awardsHandler;
+ });
}
return awardsHandlerPromise;
}
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 5758588e82e..993206b2e73 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CIIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
/**
* CommitItem
@@ -29,6 +30,7 @@ export default {
ClipboardButton,
CIIcon,
TimeAgoTooltip,
+ CommitPipelineStatus,
},
props: {
commit: {
@@ -102,6 +104,14 @@ export default {
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">
+ <div
+ v-if="commit.signatureHtml"
+ v-html="commit.signatureHtml"
+ ></div>
+ <commit-pipeline-status
+ v-if="commit.pipelineStatusPath"
+ :endpoint="commit.pipelineStatusPath"
+ />
<div class="commit-sha-group">
<div
class="label label-monospace"
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 4ae588042e4..c39403f1021 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) {
oldLine,
newLine,
lineCode,
+ positionType: 'text',
};
}
});
@@ -259,8 +260,8 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
const { lineCode, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) {
- const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter);
- const refs = convertObjectPropsToCamelCase(discussion.position.formatter);
+ const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
+ const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 11e3b781e5a..a1d8e531940 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -466,7 +466,9 @@ export default {
class="gl-responsive-table-row"
role="row">
<div
- class="table-section section-wrap section-15"
+ v-tooltip
+ :title="model.name"
+ class="table-section section-wrap section-15 text-truncate"
role="gridcell"
>
<div
@@ -480,9 +482,7 @@ export default {
v-if="!model.isFolder"
class="environment-name table-mobile-content">
<a
- v-tooltip
:href="environmentPath"
- :title="model.name"
>
{{ model.name }}
</a>
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 4ae3a714bee..175d0b8498b 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,5 +1,9 @@
import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
+import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
+import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
/**
* Updates todo counter when todos are toggled.
@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
+ const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
+
+ if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
+ Vue.use(Translate);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: setStatusModalTriggerEl,
+ data() {
+ const { hasStatus } = this.$options.el.dataset;
+
+ return {
+ hasStatus: hasStatus === 'true',
+ };
+ },
+ render(createElement) {
+ return createElement(SetStatusModalTrigger, {
+ props: {
+ hasStatus: this.hasStatus,
+ },
+ });
+ },
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: setStatusModalWrapperEl,
+ data() {
+ const { currentEmoji, currentMessage } = this.$options.el.dataset;
+
+ return {
+ currentEmoji,
+ currentMessage,
+ };
+ },
+ render(createElement) {
+ const { currentEmoji, currentMessage } = this;
+
+ return createElement(SetStatusModalWrapper, {
+ props: {
+ currentEmoji,
+ currentMessage,
+ },
+ });
+ },
+ });
+ }
+});
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 39a4ff159e2..4b1788a1c16 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -46,7 +46,7 @@
v-if="mergeRequest"
:href="mergeRequest.path"
class="js-link-commit link-commit"
- >{{ mergeRequest.iid }}</a>
+ >!{{ mergeRequest.iid }}</a>
</p>
<p class="build-light-text append-bottom-0">
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index ff45a5b05f8..ff500eddddb 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -27,7 +27,7 @@
value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'title'))
+ Object.prototype.hasOwnProperty.call(value, 'button_title'))
);
},
},
@@ -67,7 +67,7 @@
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
- {{ action.title }}
+ {{ action.button_title }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index bac8bd71d64..047e55866ce 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -2,6 +2,7 @@
import { mapGetters, mapState } from 'vuex';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
+ import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
import StuckBlock from './stuck_block.vue';
@@ -11,6 +12,7 @@
components: {
CiHeader,
Callout,
+ EmptyState,
EnvironmentsBlock,
ErasedBlock,
StuckBlock,
@@ -31,6 +33,8 @@
'jobHasStarted',
'hasEnvironment',
'isJobStuck',
+ 'hasTrace',
+ 'emptyStateIllustration',
]),
},
};
@@ -77,12 +81,14 @@
<environments-block
v-if="hasEnvironment"
+ class="js-job-environment"
:deployment-status="job.deployment_status"
:icon-status="job.status"
/>
<erased-block
v-if="job.erased"
+ class="js-job-erased"
:user="job.erased_by"
:erased-at="job.erased_at"
/>
@@ -91,6 +97,15 @@
<!-- EO job log -->
<!--empty state -->
+ <empty-state
+ v-if="!hasTrace"
+ class="js-job-empty-state"
+ :illustration-path="emptyStateIllustration.image"
+ :illustration-size-class="emptyStateIllustration.size"
+ :title="emptyStateIllustration.title"
+ :content="emptyStateIllustration.content"
+ :action="job.status.action"
+ />
<!-- EO empty state -->
<!-- EO Body Section -->
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 62d154ff584..afe5f88b292 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -30,13 +30,24 @@ export const jobHasStarted = state => !(state.job.started === false);
export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
/**
+ * Checks if it the job has trace.
+ * Used to check if it should render the job log or the empty state
+ * @returns {Boolean}
+ */
+export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running';
+
+export const emptyStateIllustration = state =>
+ (state.job && state.job.status && state.job.status.illustration) || {};
+
+/**
* When the job is pending and there are no available runners
* we need to render the stuck block;
*
* @returns {Boolean}
*/
export const isJobStuck = state =>
- state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false;
+ state.job.status.group === 'pending' &&
+ (!_.isEmpty(state.job.runners) && state.job.runners.available === false);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index d4babf1fab2..75832884711 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file
- const aLines = [a.position.formatter.new_line, a.position.formatter.old_line];
- const bLines = [b.position.formatter.new_line, b.position.formatter.old_line];
+ const aLines = [a.position.new_line, a.position.old_line];
+ const bLines = [b.position.new_line, b.position.old_line];
return filenameComparison < 0 ||
(filenameComparison === 0 &&
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 9aa83ce6269..72f3f70b98f 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -79,10 +79,13 @@ export default class Todos {
.then(({ data }) => {
this.updateRowState(target);
this.updateBadges(data);
- }).catch(() => flash(__('Error updating todo status.')));
+ }).catch(() => {
+ this.updateRowState(target, true);
+ return flash(__('Error updating todo status.'));
+ });
}
- updateRowState(target) {
+ updateRowState(target, isInactive = false) {
const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo');
const doneBtn = row.querySelector('.js-done-todo');
@@ -91,7 +94,10 @@ export default class Todos {
target.removeAttribute('disabled');
target.classList.remove('disabled');
- if (target === doneBtn) {
+ if (isInactive === true) {
+ restoreBtn.classList.add('hidden');
+ doneBtn.classList.remove('hidden');
+ } else if (target === doneBtn) {
row.classList.add('done-reversible');
restoreBtn.classList.remove('hidden');
} else if (target === restoreBtn) {
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index aea7b649c20..c7ce4675573 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
- const toggleNoEmojiPlaceholder = (isVisible) => {
+ const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
})
- .catch(() => createFlash('Failed to load emoji list!'));
+ .catch(() => createFlash('Failed to load emoji list.'));
});
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 9892a039941..bf592ba7a3c 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -43,7 +43,15 @@ const initColorKey = () =>
.domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
+ constructor(
+ container,
+ activitiesContainer,
+ timestamps,
+ calendarActivitiesPath,
+ utcOffset = 0,
+ firstDayOfWeek = 0,
+ monthsAgo = 12,
+ ) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
@@ -66,6 +74,8 @@ export default class ActivityCalendar {
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
+ this.activitiesContainer = activitiesContainer;
+ this.container = container;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@@ -75,13 +85,13 @@ export default class ActivityCalendar {
const today = getSystemDate(utcOffset);
today.setHours(0, 0, 0, 0, 0);
- const oneYearAgo = new Date(today);
- oneYearAgo.setFullYear(today.getFullYear() - 1);
+ const timeAgo = new Date(today);
+ timeAgo.setMonth(today.getMonth() - monthsAgo);
- const days = getDayDifference(oneYearAgo, today);
+ const days = getDayDifference(timeAgo, today);
for (let i = 0; i <= days; i += 1) {
- const date = new Date(oneYearAgo);
+ const date = new Date(timeAgo);
date.setDate(date.getDate() + i);
const day = date.getDay();
@@ -280,7 +290,7 @@ export default class ActivityCalendar {
this.currentSelectedDate.getDate(),
].join('-');
- $('.user-calendar-activities').html(LOADING_HTML);
+ $(this.activitiesContainer).html(LOADING_HTML);
axios
.get(this.calendarActivitiesPath, {
@@ -289,11 +299,11 @@ export default class ActivityCalendar {
},
responseType: 'text',
})
- .then(({ data }) => $('.user-calendar-activities').html(data))
+ .then(({ data }) => $(this.activitiesContainer).html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
- $('.user-calendar-activities').html('');
+ $(this.activitiesContainer).html('');
}
}
}
diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js
new file mode 100644
index 00000000000..0009419cd0c
--- /dev/null
+++ b/app/assets/javascripts/pages/users/user_overview_block.js
@@ -0,0 +1,42 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default class UserOverviewBlock {
+ constructor(options = {}) {
+ this.container = options.container;
+ this.url = options.url;
+ this.limit = options.limit || 20;
+ this.loadData();
+ }
+
+ loadData() {
+ const loadingEl = document.querySelector(`${this.container} .loading`);
+
+ loadingEl.classList.remove('hide');
+
+ axios
+ .get(this.url, {
+ params: {
+ limit: this.limit,
+ },
+ })
+ .then(({ data }) => this.render(data))
+ .catch(() => loadingEl.classList.add('hide'));
+ }
+
+ render(data) {
+ const { html, count } = data;
+ const contentList = document.querySelector(`${this.container} .overview-content-list`);
+
+ contentList.innerHTML += html;
+
+ const loadingEl = document.querySelector(`${this.container} .loading`);
+
+ if (count && count > 0) {
+ document.querySelector(`${this.container} .js-view-all`).classList.remove('hide');
+ } else {
+ document.querySelector(`${this.container} .nothing-here-block`).classList.add('text-left', 'p-0');
+ }
+
+ loadingEl.classList.add('hide');
+ }
+}
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index a2ca03536f2..23b0348a99f 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -2,9 +2,10 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import flash from '~/flash';
import ActivityCalendar from './activity_calendar';
+import UserOverviewBlock from './user_overview_block';
/**
* UserTabs
@@ -61,19 +62,28 @@ import ActivityCalendar from './activity_calendar';
* </div>
*/
-const CALENDAR_TEMPLATE = `
- <div class="clearfix calendar">
- <div class="js-contrib-calendar"></div>
- <div class="calendar-hint">
- Summary of issues, merge requests, push events, and comments
+const CALENDAR_TEMPLATES = {
+ activity: `
+ <div class="clearfix calendar">
+ <div class="js-contrib-calendar"></div>
+ <div class="calendar-hint bottom-right"></div>
</div>
- </div>
-`;
+ `,
+ overview: `
+ <div class="clearfix calendar">
+ <div class="calendar-hint"></div>
+ <div class="js-contrib-calendar prepend-top-20"></div>
+ </div>
+ `,
+};
+
+const CALENDAR_PERIOD_6_MONTHS = 6;
+const CALENDAR_PERIOD_12_MONTHS = 12;
export default class UserTabs {
constructor({ defaultAction, action, parentEl }) {
this.loaded = {};
- this.defaultAction = defaultAction || 'activity';
+ this.defaultAction = defaultAction || 'overview';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location;
@@ -124,6 +134,8 @@ export default class UserTabs {
}
if (action === 'activity') {
this.loadActivities();
+ } else if (action === 'overview') {
+ this.loadOverviewTab();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
@@ -154,7 +166,40 @@ export default class UserTabs {
if (this.loaded.activity) {
return;
}
- const $calendarWrap = this.$parentEl.find('.user-calendar');
+
+ this.loadActivityCalendar('activity');
+
+ // eslint-disable-next-line no-new
+ new Activities();
+
+ this.loaded.activity = true;
+ }
+
+ loadOverviewTab() {
+ if (this.loaded.overview) {
+ return;
+ }
+
+ this.loadActivityCalendar('overview');
+
+ UserTabs.renderMostRecentBlocks('#js-overview .activities-block', 5);
+ UserTabs.renderMostRecentBlocks('#js-overview .projects-block', 10);
+
+ this.loaded.overview = true;
+ }
+
+ static renderMostRecentBlocks(container, limit) {
+ // eslint-disable-next-line no-new
+ new UserOverviewBlock({
+ container,
+ url: $(`${container} .overview-content-list`).data('href'),
+ limit,
+ });
+ }
+
+ loadActivityCalendar(action) {
+ const monthsAgo = action === 'overview' ? CALENDAR_PERIOD_6_MONTHS : CALENDAR_PERIOD_12_MONTHS;
+ const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
const calendarPath = $calendarWrap.data('calendarPath');
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
const utcOffset = $calendarWrap.data('utcOffset');
@@ -166,17 +211,22 @@ export default class UserTabs {
axios
.get(calendarPath)
.then(({ data }) => {
- $calendarWrap.html(CALENDAR_TEMPLATE);
- $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
+ $calendarWrap.html(CALENDAR_TEMPLATES[action]);
+
+ let calendarHint = '';
+
+ if (action === 'activity') {
+ calendarHint = sprintf(__('Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})'), { utcFormatted });
+ } else if (action === 'overview') {
+ calendarHint = __('Issues, merge requests, pushes and comments.');
+ }
+
+ $calendarWrap.find('.calendar-hint').text(calendarHint);
// eslint-disable-next-line no-new
- new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset);
+ new ActivityCalendar('.tab-pane.active .js-contrib-calendar', '.tab-pane.active .user-calendar-activities', data, calendarActivitiesPath, utcOffset, 0, monthsAgo);
})
.catch(() => flash(__('There was an error loading users activity calendar.')));
-
- // eslint-disable-next-line no-new
- new Activities();
- this.loaded.activity = true;
}
toggleLoading(status) {
diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
new file mode 100644
index 00000000000..14a89ef9293
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
@@ -0,0 +1,21 @@
+import { AwardsHandler } from '~/awards_handler';
+
+class EmojiMenuInModal extends AwardsHandler {
+ constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
+ super(emoji);
+
+ this.selectEmojiCallback = selectEmojiCallback;
+ this.toggleButtonSelector = toggleButtonSelector;
+ this.menuClass = menuClass;
+ this.targetContainerEl = targetContainerEl;
+
+ this.bindEvents();
+ }
+
+ postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
+ this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
+ callback();
+ }
+}
+
+export default EmojiMenuInModal;
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
new file mode 100644
index 00000000000..48e5ede80f2
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
@@ -0,0 +1,33 @@
+<script>
+import { s__ } from '~/locale';
+import eventHub from './event_hub';
+
+export default {
+ props: {
+ hasStatus: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ buttonText() {
+ return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
+ },
+ },
+ methods: {
+ openModal() {
+ eventHub.$emit('openModal');
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn menu-item"
+ @click="openModal"
+ >
+ {{ buttonText }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
new file mode 100644
index 00000000000..43f0b6651b9
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -0,0 +1,241 @@
+<script>
+import $ from 'jquery';
+import createFlash from '~/flash';
+import Icon from '~/vue_shared/components/icon.vue';
+import GfmAutoComplete from '~/gfm_auto_complete';
+import { __, s__ } from '~/locale';
+import Api from '~/api';
+import eventHub from './event_hub';
+import EmojiMenuInModal from './emoji_menu_in_modal';
+
+const emojiMenuClass = 'js-modal-status-emoji-menu';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ currentEmoji: {
+ type: String,
+ required: true,
+ },
+ currentMessage: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ defaultEmojiTag: '',
+ emoji: this.currentEmoji,
+ emojiMenu: null,
+ emojiTag: '',
+ isEmojiMenuVisible: false,
+ message: this.currentMessage,
+ modalId: 'set-user-status-modal',
+ noEmoji: true,
+ };
+ },
+ computed: {
+ isDirty() {
+ return this.message.length || this.emoji.length;
+ },
+ },
+ mounted() {
+ eventHub.$on('openModal', this.openModal);
+ },
+ beforeDestroy() {
+ this.emojiMenu.destroy();
+ },
+ methods: {
+ openModal() {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ },
+ closeModal() {
+ this.$root.$emit('bv::hide::modal', this.modalId);
+ },
+ setupEmojiListAndAutocomplete() {
+ const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
+ const emojiAutocomplete = new GfmAutoComplete();
+ emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
+
+ import(/* webpackChunkName: 'emoji' */ '~/emoji')
+ .then(Emoji => {
+ if (this.emoji) {
+ this.emojiTag = Emoji.glEmojiTag(this.emoji);
+ }
+ this.noEmoji = this.emoji === '';
+ this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
+
+ this.emojiMenu = new EmojiMenuInModal(
+ Emoji,
+ toggleEmojiMenuButtonSelector,
+ emojiMenuClass,
+ this.setEmoji,
+ this.$refs.userStatusForm,
+ );
+ })
+ .catch(() => createFlash(__('Failed to load emoji list.')));
+ },
+ showEmojiMenu() {
+ this.isEmojiMenuVisible = true;
+ this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
+ },
+ hideEmojiMenu() {
+ if (!this.isEmojiMenuVisible) {
+ return;
+ }
+
+ this.isEmojiMenuVisible = false;
+ this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
+ },
+ setDefaultEmoji() {
+ const { emojiTag } = this;
+ const hasStatusMessage = this.message;
+ if (hasStatusMessage && emojiTag) {
+ return;
+ }
+
+ if (hasStatusMessage) {
+ this.noEmoji = false;
+ this.emojiTag = this.defaultEmojiTag;
+ } else if (emojiTag === this.defaultEmojiTag) {
+ this.noEmoji = true;
+ this.clearEmoji();
+ }
+ },
+ setEmoji(emoji, emojiTag) {
+ this.emoji = emoji;
+ this.noEmoji = false;
+ this.clearEmoji();
+ this.emojiTag = emojiTag;
+ },
+ clearEmoji() {
+ if (this.emojiTag) {
+ this.emojiTag = '';
+ }
+ },
+ clearStatusInputs() {
+ this.emoji = '';
+ this.message = '';
+ this.noEmoji = true;
+ this.clearEmoji();
+ this.hideEmojiMenu();
+ },
+ removeStatus() {
+ this.clearStatusInputs();
+ this.setStatus();
+ },
+ setStatus() {
+ const { emoji, message } = this;
+
+ Api.postUserStatus({
+ emoji,
+ message,
+ })
+ .then(this.onUpdateSuccess)
+ .catch(this.onUpdateFail);
+ },
+ onUpdateSuccess() {
+ this.closeModal();
+ window.location.reload();
+ },
+ onUpdateFail() {
+ createFlash(
+ s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."),
+ );
+
+ this.closeModal();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-ui-modal
+ :title="s__('SetStatusModal|Set a status')"
+ :modal-id="modalId"
+ :ok-title="s__('SetStatusModal|Set status')"
+ :cancel-title="s__('SetStatusModal|Remove status')"
+ ok-variant="success"
+ class="set-user-status-modal"
+ @shown="setupEmojiListAndAutocomplete"
+ @hide="hideEmojiMenu"
+ @ok="setStatus"
+ @cancel="removeStatus"
+ >
+ <div>
+ <input
+ v-model="emoji"
+ class="js-status-emoji-field"
+ type="hidden"
+ name="user[status][emoji]"
+ />
+ <div
+ ref="userStatusForm"
+ class="form-group position-relative m-0"
+ >
+ <div class="input-group">
+ <span class="input-group-btn">
+ <button
+ ref="toggleEmojiMenuButton"
+ v-gl-tooltip.bottom
+ :title="s__('SetStatusModal|Add status emoji')"
+ :aria-label="s__('SetStatusModal|Add status emoji')"
+ name="button"
+ type="button"
+ class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
+ @click="showEmojiMenu"
+ >
+ <span v-html="emojiTag"></span>
+ <span
+ v-show="noEmoji"
+ class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
+ >
+ <icon
+ name="emoji_slightly_smiling_face"
+ css-classes="award-control-icon-neutral"
+ />
+ <icon
+ name="emoji_smiley"
+ css-classes="award-control-icon-positive"
+ />
+ <icon
+ name="emoji_smile"
+ css-classes="award-control-icon-super-positive"
+ />
+ </span>
+ </button>
+ </span>
+ <input
+ ref="statusMessageField"
+ v-model="message"
+ :placeholder="s__('SetStatusModal|What\'s your status?')"
+ type="text"
+ class="form-control form-control input-lg js-status-message-field"
+ name="user[status][message]"
+ @keyup="setDefaultEmoji"
+ @keyup.enter.prevent
+ @click="hideEmojiMenu"
+ />
+ <span
+ v-show="isDirty"
+ class="input-group-btn"
+ >
+ <button
+ v-gl-tooltip.bottom
+ :title="s__('SetStatusModal|Clear status')"
+ :aria-label="s__('SetStatusModal|Clear status')"
+ name="button"
+ type="button"
+ class="js-clear-user-status-button clear-user-status btn"
+ @click="clearStatusInputs()"
+ >
+ <icon name="close" />
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+ </gl-ui-modal>
+</template>
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index e19bbbacf4d..b9dfa22dd49 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -592,7 +592,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
var trimmed = query.term.trim();
emailUser = {
- name: "Invite \"" + query.term + "\" by email",
+ name: "Invite \"" + trimmed + "\" by email",
username: trimmed,
id: trimmed,
invite: true
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 0b9dff64b0b..9638fee6078 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,8 +1,7 @@
-.calender-block {
+.calendar-block {
padding-left: 0;
padding-right: 0;
border-top: 0;
- direction: rtl;
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
overflow-x: auto;
@@ -42,10 +41,13 @@
}
.calendar-hint {
- margin-top: -23px;
- float: right;
font-size: 12px;
- direction: ltr;
+
+ &.bottom-right {
+ direction: ltr;
+ margin-top: -23px;
+ float: right;
+ }
}
.pika-single.gitlab-theme {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 11a30d83f03..c430009bfe0 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -529,9 +529,10 @@
}
.header-user {
- .dropdown-menu {
+ &.show .dropdown-menu {
width: auto;
min-width: unset;
+ max-height: 323px;
margin-top: 4px;
color: $gl-text-color;
left: auto;
@@ -542,6 +543,18 @@
.user-name {
display: block;
}
+
+ .user-status-emoji {
+ margin-right: 0;
+ display: block;
+ vertical-align: text-top;
+ max-width: 148px;
+ font-size: 12px;
+
+ gl-emoji {
+ font-size: $gl-font-size;
+ }
+ }
}
svg {
@@ -573,3 +586,24 @@
}
}
}
+
+.set-user-status-modal {
+ .modal-body {
+ min-height: unset;
+ }
+
+ .input-lg {
+ max-width: unset;
+ }
+
+ .no-emoji-placeholder,
+ .clear-user-status {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+
+ .emoji-menu-toggle-button {
+ @include emoji-menu-toggle-button;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 7f37dd3de91..be41dbfc61f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -20,20 +20,24 @@
display: inline-block;
overflow-x: auto;
border: 0;
- border-color: $gray-100;
+ border-color: $gl-gray-100;
@supports (width: fit-content) {
display: block;
width: fit-content;
}
+ tbody {
+ background-color: $white-light;
+ }
+
tr {
th {
- border-bottom: solid 2px $gray-100;
+ border-bottom: solid 2px $gl-gray-100;
}
td {
- border-color: $gray-100;
+ border-color: $gl-gray-100;
}
}
}
@@ -266,3 +270,59 @@
border-radius: 50%;
}
}
+
+@mixin emoji-menu-toggle-button {
+ line-height: 1;
+ padding: 0;
+ min-width: 16px;
+ color: $gray-darkest;
+ fill: $gray-darkest;
+
+ .fa {
+ position: relative;
+ font-size: 16px;
+ }
+
+ svg {
+ @include btn-svg;
+ margin: 0;
+ }
+
+ .award-control-icon-positive,
+ .award-control-icon-super-positive {
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ }
+
+ &:hover,
+ &.is-active {
+ .danger-highlight {
+ color: $red-500;
+ }
+
+ .link-highlight {
+ color: $blue-600;
+ fill: $blue-600;
+ }
+
+ .award-control-icon-neutral {
+ opacity: 0;
+ }
+
+ .award-control-icon-positive {
+ opacity: 1;
+ }
+ }
+
+ &.is-active {
+ .award-control-icon-positive {
+ opacity: 0;
+ }
+
+ .award-control-icon-super-positive {
+ opacity: 1;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 837016db67b..b7a95f604b8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -314,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
/*
* Dropdowns
@@ -634,5 +635,4 @@ Modals
*/
$modal-body-height: 134px;
-
$priority-label-empty-state-width: 114px;
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 7d90452e1f4..759b4f333ca 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -18,3 +18,4 @@ $success: $green-500;
$info: $blue-500;
$warning: $orange-500;
$danger: $red-500;
+$zindex-modal-backdrop: 1040;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index c9e0899425f..a2070087963 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -519,59 +519,7 @@ ul.notes {
}
.note-action-button {
- line-height: 1;
- padding: 0;
- min-width: 16px;
- color: $gray-darkest;
- fill: $gray-darkest;
-
- .fa {
- position: relative;
- font-size: 16px;
- }
-
- svg {
- @include btn-svg;
- margin: 0;
- }
-
- .award-control-icon-positive,
- .award-control-icon-super-positive {
- position: absolute;
- top: 0;
- left: 0;
- opacity: 0;
- }
-
- &:hover,
- &.is-active {
- .danger-highlight {
- color: $red-500;
- }
-
- .link-highlight {
- color: $blue-600;
- fill: $blue-600;
- }
-
- .award-control-icon-neutral {
- opacity: 0;
- }
-
- .award-control-icon-positive {
- opacity: 1;
- }
- }
-
- &.is-active {
- .award-control-icon-positive {
- opacity: 0;
- }
-
- .award-control-icon-super-positive {
- opacity: 1;
- }
- }
+ @include emoji-menu-toggle-button;
}
.discussion-toggle-button {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index caa839c32a5..f084adaf5d3 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items.
.middle-dot-divider {
&::after {
- content: "\00B7"; // Middle Dot
+ content: '\00B7'; // Middle Dot
padding: 0 6px;
font-weight: $gl-font-weight-bold;
}
&:last-child {
&::after {
- content: "";
+ content: '';
padding: 0;
}
}
@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) {
width: auto;
}
-
}
.profile-crop-image-container {
@@ -215,7 +214,6 @@
}
}
-
.user-profile {
.cover-controls a {
margin-left: 5px;
@@ -418,7 +416,7 @@ table.u2f-registrations {
}
&.unverified {
- @include status-color($gray-dark, color("gray"), $common-gray-dark);
+ @include status-color($gray-dark, color('gray'), $common-gray-dark);
}
}
}
@@ -431,7 +429,7 @@ table.u2f-registrations {
}
.emoji-menu-toggle-button {
- @extend .note-action-button;
+ @include emoji-menu-toggle-button;
.no-emoji-placeholder {
position: relative;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 7c42dcad959..da3d8aa53ad 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -830,6 +830,14 @@
}
}
+.repository-language-bar-tooltip-language {
+ font-weight: $gl-font-weight-bold;
+}
+
+.repository-language-bar-tooltip-share {
+ color: $theme-gray-400;
+}
+
pre.light-well {
border-color: $well-light-border;
}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 2e2ab8532d2..59fdbf31fe9 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -1,4 +1,5 @@
@import 'framework/variables';
+@import 'framework/variables_overrides';
@import 'peek/views/rblineprof';
#js-peek {
@@ -6,7 +7,7 @@
left: 0;
top: 0;
width: 100%;
- z-index: 1039;
+ z-index: #{$zindex-modal-backdrop + 1};
height: $performance-bar-height;
background: $black;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index b7c758a42ed..8040a14ef56 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -67,8 +67,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
- def reset_runners_token
+ def reset_registration_token
@application_setting.reset_runners_registration_token!
+
flash[:notice] = 'New runners registration token has been generated!'
redirect_to admin_runners_path
end
diff --git a/app/controllers/concerns/labels_as_hash.rb b/app/controllers/concerns/labels_as_hash.rb
new file mode 100644
index 00000000000..1171aa9cf44
--- /dev/null
+++ b/app/controllers/concerns/labels_as_hash.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module LabelsAsHash
+ extend ActiveSupport::Concern
+
+ def labels_as_hash(target = nil, params = {})
+ available_labels = LabelsFinder.new(
+ current_user,
+ params
+ ).execute
+
+ label_hashes = available_labels.as_json(only: [:title, :color])
+
+ if target&.respond_to?(:labels)
+ already_set_labels = available_labels & target.labels
+ if already_set_labels.present?
+ titles = already_set_labels.map(&:title)
+ label_hashes.each do |hash|
+ if titles.include?(hash['title'])
+ hash[:set] = true
+ end
+ end
+ end
+ end
+
+ label_hashes
+ end
+end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index cb9ab35de85..26768c628ca 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -12,7 +12,8 @@ class Groups::LabelsController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
- @labels = GroupLabelsFinder.new(@group, params.merge(sort: sort)).execute
+ @labels = GroupLabelsFinder
+ .new(current_user, @group, params.merge(sort: sort)).execute
end
format.json do
render json: LabelSerializer.new.represent_appearance(available_labels)
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 6d9a225b771..93f3eb2be6d 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -10,6 +10,13 @@ module Groups
define_secret_variables
end
+ def reset_registration_token
+ @group.reset_runners_token!
+
+ flash[:notice] = 'New runners registration token has been generated!'
+ redirect_to group_settings_ci_cd_path
+ end
+
private
def define_secret_variables
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index a0ce3b08d9f..640038818f2 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -163,6 +163,7 @@ class Projects::LabelsController < Projects::ApplicationController
project_id: @project.id,
include_ancestor_groups: params[:include_ancestor_groups],
search: params[:search],
+ subscribed: params[:subscribed],
sort: sort).execute
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 25d2c11b7db..5307cd0720a 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -25,7 +25,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@diffs.write_cache
- render json: DiffsSerializer.new(current_user: current_user, project: @merge_request.project).represent(@diffs, additional_attributes)
+ request = {
+ current_user: current_user,
+ project: @merge_request.project,
+ render: ->(partial, locals) { view_to_html_string(partial, locals) }
+ }
+
+ render json: DiffsSerializer.new(request).represent(@diffs, additional_attributes)
end
def define_diff_vars
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index a2d1b7866c2..3a1344651df 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -36,6 +36,13 @@ module Projects
end
end
+ def reset_registration_token
+ @project.reset_runners_token!
+
+ flash[:notice] = 'New runners registration token has been generated!'
+ redirect_to namespace_project_settings_ci_cd_path
+ end
+
private
def update_params
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e509098d778..d16240af404 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -29,11 +29,17 @@ class UsersController < ApplicationController
format.json do
load_events
- pager_json("events/_events", @events.count)
+ pager_json("events/_events", @events.count, events: @events)
end
end
end
+ def activity
+ respond_to do |format|
+ format.html { render 'show' }
+ end
+ end
+
def groups
load_groups
@@ -53,9 +59,7 @@ class UsersController < ApplicationController
respond_to do |format|
format.html { render 'show' }
format.json do
- render json: {
- html: view_to_html_string("shared/projects/_list", projects: @projects)
- }
+ pager_json("shared/projects/_list", @projects.count, projects: @projects)
end
end
end
@@ -125,6 +129,7 @@ class UsersController < ApplicationController
@projects =
PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page])
+ .per(params[:limit])
prepare_projects_for_rendering(@projects)
end
diff --git a/app/finders/group_labels_finder.rb b/app/finders/group_labels_finder.rb
index 903023033ed..a668a0f0fae 100644
--- a/app/finders/group_labels_finder.rb
+++ b/app/finders/group_labels_finder.rb
@@ -1,17 +1,29 @@
# frozen_string_literal: true
class GroupLabelsFinder
- attr_reader :group, :params
+ attr_reader :current_user, :group, :params
- def initialize(group, params = {})
+ def initialize(current_user, group, params = {})
+ @current_user = current_user
@group = group
@params = params
end
def execute
group.labels
+ .optionally_subscribed_by(subscriber_id)
.optionally_search(params[:search])
.order_by(params[:sort])
.page(params[:page])
end
+
+ private
+
+ def subscriber_id
+ current_user&.id if subscribed?
+ end
+
+ def subscribed?
+ params[:subscribed] == 'true'
+ end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 08fc2968e77..82e0b2ed9e1 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -17,6 +17,7 @@ class LabelsFinder < UnionFinder
@skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none
items = with_title(items)
+ items = by_subscription(items)
items = by_search(items)
sort(items)
end
@@ -84,6 +85,18 @@ class LabelsFinder < UnionFinder
labels.search(params[:search])
end
+ def by_subscription(labels)
+ labels.optionally_subscribed_by(subscriber_id)
+ end
+
+ def subscriber_id
+ current_user&.id if subscribed?
+ end
+
+ def subscribed?
+ params[:subscribed] == 'true'
+ end
+
# Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group.
def group_ids_for(group)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 50c051c3aa1..e190d5d90c9 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -66,10 +66,6 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
- def item_project_ids(items)
- items&.reorder(nil)&.select(:target_project_id)
- end
-
def by_wip(items)
if params[:wip] == 'yes'
items.where(wip_match(items.arel_table))
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index eeca5026da1..3f2e813d381 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -31,7 +31,7 @@ class UserRecentEventsFinder
recent_events(params[:offset] || 0)
.joins(:project)
.with_associations
- .limit_recent(LIMIT, params[:offset])
+ .limit_recent(params[:limit].presence || LIMIT, params[:offset])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index e3b74f443f7..be1e7016a1e 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -59,8 +59,8 @@ module BoardsHelper
{
toggle: "dropdown",
- list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
- labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
+ list_labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_ancestor_groups: true),
+ labels: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.path,
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index a67c91b21d7..19eb763e1de 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -13,8 +13,4 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner'
end
end
-
- def rbac_clusters_feature_enabled?
- Feature.enabled?(:rbac_clusters)
- end
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 6c51739ba1a..76ed8efe2c6 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -131,20 +131,26 @@ module LabelsHelper
end
end
- def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
- project = @target_project || @project
-
+ def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false)
options = {}
options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
+ options[:only_group_labels] = only_group_labels if only_group_labels && @group
+ options[:format] = :json
+
+ labels_filter_path(options)
+ end
+
+ def labels_filter_path(options = {})
+ project = @target_project || @project
+ format = options.delete(:format) || :html
if project
- project_labels_path(project, :json, options)
+ project_labels_path(project, format, options)
elsif @group
- options[:only_group_labels] = only_group_labels if only_group_labels
- group_labels_path(@group, :json, options)
+ group_labels_path(@group, format, options)
else
- dashboard_labels_path(:json)
+ dashboard_labels_path(format, options)
end
end
diff --git a/app/helpers/repository_languages_helper.rb b/app/helpers/repository_languages_helper.rb
index c1505b52808..cf7eee7fff3 100644
--- a/app/helpers/repository_languages_helper.rb
+++ b/app/helpers/repository_languages_helper.rb
@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper
content_tag :div, nil,
class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}",
- title: lang.name
+ data: { html: true },
+ title: "<span class=\"repository-language-bar-tooltip-language\">#{escape_javascript(lang.name)}</span>&nbsp;<span class=\"repository-language-bar-tooltip-share\">#{lang.share.round(1)}%</span>"
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index bcd91f619c8..42b533ad772 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -76,7 +76,7 @@ module UsersHelper
tabs = []
if can?(current_user, :read_user_profile, @user)
- tabs += [:activity, :groups, :contributed, :projects, :snippets]
+ tabs += [:overview, :activity, :groups, :contributed, :projects, :snippets]
end
tabs
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 765943104a0..d986e33280d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -633,6 +633,10 @@ module Ci
end
end
+ def default_branch?
+ ref == project.default_branch
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 49c36ad9d3f..a61ed03cf35 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -319,7 +319,11 @@ class Commit
def status(ref = nil)
return @statuses[ref] if @statuses.key?(ref)
- @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
+ @statuses[ref] = status_for_project(ref, project)
+ end
+
+ def status_for_project(ref, pipeline_project)
+ pipeline_project.pipelines.latest_status_per_commit(id, ref)[id]
end
def set_status_for_ref(ref, status)
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 1d0a61364b0..92a5c1112af 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -31,9 +31,11 @@ module Subscribable
end
def subscribers(project)
- subscriptions_available(project)
- .where(subscribed: true)
- .map(&:user)
+ relation = subscriptions_available(project)
+ .where(subscribed: true)
+ .select(:user_id)
+
+ User.where(id: relation)
end
def toggle_subscription(user, project = nil)
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 7d8ce0bbd05..11289887e00 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -64,10 +64,10 @@ class InstanceConfiguration
end
def ssh_algorithm_md5(ssh_file_content)
- OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
+ Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint
end
def ssh_algorithm_sha256(ssh_file_content)
- OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
+ Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 9ef57a05b3e..43b49445765 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -45,6 +45,7 @@ class Label < ActiveRecord::Base
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
scope :order_name_asc, -> { reorder(title: :asc) }
scope :order_name_desc, -> { reorder(title: :desc) }
+ scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
def self.prioritized(project)
joins(:priorities)
@@ -74,6 +75,14 @@ class Label < ActiveRecord::Base
joins(label_priorities)
end
+ def self.optionally_subscribed_by(user_id)
+ if user_id
+ subscribed_by(user_id)
+ else
+ all
+ end
+ end
+
alias_attribute :name, :title
def self.reference_prefix
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index d0e84b1aa38..f2c246cd969 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -390,7 +390,11 @@ class ProjectPolicy < BasePolicy
greedy_load_subject ||= !@user.persisted?
if greedy_load_subject
- project.team.members.include?(user)
+ # We want to load all the members with one query. Calling #include? on
+ # project.team.members will perform a separate query for each user, unless
+ # project.team.members was loaded before somewhere else. Calling #to_a
+ # ensures it's always loaded before checking for membership.
+ project.team.members.to_a.include?(user)
else
# otherwise we just make a specific query for
# this particular user.
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index 396e95a03c8..a94e32478ce 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -25,4 +25,25 @@ class CommitEntity < API::Entities::Commit
expose :title_html, if: { type: :full } do |commit|
markdown_field(commit, :title)
end
+
+ expose :signature_html, if: { type: :full } do |commit|
+ render('projects/commit/_signature', signature: commit.signature) if commit.has_signature?
+ end
+
+ expose :pipeline_status_path, if: { type: :full } do |commit, options|
+ pipeline_ref = options[:pipeline_ref]
+ pipeline_project = options[:pipeline_project] || commit.project
+ next unless pipeline_ref && pipeline_project
+
+ status = commit.status_for_project(pipeline_ref, pipeline_project)
+ next unless status
+
+ pipelines_project_commit_path(pipeline_project, commit.id, ref: pipeline_ref)
+ end
+
+ def render(*args)
+ return unless request.respond_to?(:render) && request.render.respond_to?(:call)
+
+ request.render.call(*args)
+ end
end
diff --git a/app/serializers/detailed_status_entity.rb b/app/serializers/detailed_status_entity.rb
index c772c807f76..da994d78286 100644
--- a/app/serializers/detailed_status_entity.rb
+++ b/app/serializers/detailed_status_entity.rb
@@ -10,7 +10,12 @@ class DetailedStatusEntity < Grape::Entity
expose :illustration do |status|
begin
- status.illustration
+ illustration = {
+ image: ActionController::Base.helpers.image_path(status.illustration[:image])
+ }
+ illustration = status.illustration.merge(illustration)
+
+ illustration
rescue NotImplementedError
# ignored
end
@@ -25,5 +30,6 @@ class DetailedStatusEntity < Grape::Entity
expose :action_title, as: :title
expose :action_path, as: :path
expose :action_method, as: :method
+ expose :action_button_title, as: :button_title
end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index 00dc55fc004..b51e4a7e6d0 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -18,7 +18,9 @@ class DiffsEntity < Grape::Entity
expose :commit do |diffs, options|
CommitEntity.represent options[:commit], options.merge(
type: :full,
- commit_url_params: { merge_request_iid: merge_request&.iid }
+ commit_url_params: { merge_request_iid: merge_request&.iid },
+ pipeline_ref: merge_request&.source_branch,
+ pipeline_project: merge_request&.source_project
)
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 5286b92ab6b..7b747171d9c 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -2,6 +2,7 @@
module Projects
class AutocompleteService < BaseService
+ include LabelsAsHash
def issues
IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
@@ -22,34 +23,14 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels_as_hash(target = nil)
- available_labels = LabelsFinder.new(
- current_user,
- project_id: project.id,
- include_ancestor_groups: true
- ).execute
-
- label_hashes = available_labels.as_json(only: [:title, :color])
-
- if target&.respond_to?(:labels)
- already_set_labels = available_labels & target.labels
- if already_set_labels.present?
- titles = already_set_labels.map(&:title)
- label_hashes.each do |hash|
- if titles.include?(hash['title'])
- hash[:set] = true
- end
- end
- end
- end
-
- label_hashes
- end
-
def commands(noteable, type)
return [] unless noteable
QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end
+
+ def labels_as_hash(target)
+ super(target, project_id: project.id, include_ancestor_groups: true)
+ end
end
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 8933bef29ee..defa579f9a8 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -210,9 +210,14 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
- available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
+ if project
+ available_labels = LabelsFinder
+ .new(current_user, project_id: project.id, include_ancestor_groups: true)
+ .execute
+ end
- current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+ project &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
end
command :label do |labels_param|
@@ -286,7 +291,8 @@ module QuickActions
end
params '#issue | !merge_request'
condition do
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ [MergeRequest, Issue].include?(issuable.class) &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
@@ -443,7 +449,8 @@ module QuickActions
end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
- current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ issuable.is_a?(TimeTrackable) &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |raw_time_date|
Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
@@ -493,7 +500,7 @@ module QuickActions
desc "Lock the discussion"
explanation "Locks the discussion"
condition do
- issuable.is_a?(Issuable) &&
+ [MergeRequest, Issue].include?(issuable.class) &&
issuable.persisted? &&
!issuable.discussion_locked? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
@@ -505,7 +512,7 @@ module QuickActions
desc "Unlock the discussion"
explanation "Unlocks the discussion"
condition do
- issuable.is_a?(Issuable) &&
+ [MergeRequest, Issue].include?(issuable.class) &&
issuable.persisted? &&
issuable.discussion_locked? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index ccba1c461fc..32f6eefc16a 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,6 +1,8 @@
- add_to_breadcrumbs "Projects", admin_projects_path
- breadcrumb_title @project.full_name
- page_title @project.full_name, "Projects"
+- @content_class = "admin-projects"
+
%h3.page-title
Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr float-right" do
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index a5326f4b909..e9e4e0847d3 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -2,106 +2,103 @@
- @no_container = true
%div{ class: container_class }
- .bs-callout
- %p
- = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
- %br
- = _('Runners can be placed on separate users, servers, even on your local machine.')
- %br
+ .row
+ .col-sm-6
+ .bs-callout
+ %p
+ = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
+ %br
+ = _('Runners can be placed on separate users, servers, even on your local machine.')
+ %br
- %div
- %span= _('Each Runner can be in one of the following states:')
- %ul
- %li
- %span.badge.badge-success shared
- \-
- = _('Runner runs jobs from all unassigned projects')
- %li
- %span.badge.badge-success group
- \-
- = _('Runner runs jobs from all unassigned projects in its group')
- %li
- %span.badge.badge-info specific
- \-
- = _('Runner runs jobs from assigned projects')
- %li
- %span.badge.badge-warning locked
- \-
- = _('Runner cannot be assigned to other projects')
- %li
- %span.badge.badge-danger paused
- \-
- = _('Runner will not receive any new jobs')
+ %div
+ %span= _('Each Runner can be in one of the following states:')
+ %ul
+ %li
+ %span.badge.badge-success shared
+ \-
+ = _('Runner runs jobs from all unassigned projects')
+ %li
+ %span.badge.badge-success group
+ \-
+ = _('Runner runs jobs from all unassigned projects in its group')
+ %li
+ %span.badge.badge-info specific
+ \-
+ = _('Runner runs jobs from assigned projects')
+ %li
+ %span.badge.badge-warning locked
+ \-
+ = _('Runner cannot be assigned to other projects')
+ %li
+ %span.badge.badge-danger paused
+ \-
+ = _('Runner will not receive any new jobs')
- .bs-callout.clearfix
- .float-left
- %p
- = _('You can reset runners registration token by pressing a button below.')
- .prepend-top-10
- = button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: _('Are you sure you want to reset registration token?') }
+ .col-sm-6
+ .bs-callout
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
+ type: 'shared',
+ reset_token_url: reset_registration_token_admin_application_settings_path }
- = render partial: 'ci/runner/how_to_setup_shared_runner',
- locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
+ .row
+ .col-sm-9
+ = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
+ .filtered-search-wrapper
+ .filtered-search-box
+ = dropdown_tag(custom_icon('icon_history'),
+ options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
+ toggle_class: 'filtered-search-history-dropdown-toggle-button',
+ dropdown_class: 'filtered-search-history-dropdown',
+ content_class: 'filtered-search-history-dropdown-content',
+ title: _('Recent searches') }) do
+ .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
+ .filtered-search-box-input-container.droplab-dropdown
+ .scroll-container
+ %ul.tokens-container.list-unstyled
+ %li.input-token
+ %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
+ #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { action: 'submit' } }
+ = button_tag class: %w[btn btn-link] do
+ = sprite_icon('search')
+ %span
+ = _('Press Enter or click to search')
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ = button_tag class: %w[btn btn-link] do
+ -# Encapsulate static class name `{{icon}}` inside #{} to bypass
+ -# haml lint's ClassAttributeWithStaticValue
+ %svg
+ %use{ 'xlink:href': "#{'{{icon}}'}" }
+ %span.js-filter-hint
+ {{hint}}
+ %span.js-filter-tag.dropdown-light-content
+ {{tag}}
- .bs-callout
- %p
- = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
+ #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ - Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ %li.filter-dropdown-item{ data: { value: status } }
+ = button_tag class: %w[btn btn-link] do
+ = status.titleize
- .row-content-block.second-block
- = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
- .filtered-search-wrapper
- .filtered-search-box
- = dropdown_tag(custom_icon('icon_history'),
- options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
- toggle_class: 'filtered-search-history-dropdown-toggle-button',
- dropdown_class: 'filtered-search-history-dropdown',
- content_class: 'filtered-search-history-dropdown-content',
- title: _('Recent searches') }) do
- .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
- .filtered-search-box-input-container.droplab-dropdown
- .scroll-container
- %ul.tokens-container.list-unstyled
- %li.input-token
- %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
- #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { action: 'submit' } }
- = button_tag class: %w[btn btn-link] do
- = sprite_icon('search')
- %span
- = _('Press Enter or click to search')
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- = button_tag class: %w[btn btn-link] do
- -# Encapsulate static class name `{{icon}}` inside #{} to bypass
- -# haml lint's ClassAttributeWithStaticValue
- %svg
- %use{ 'xlink:href': "#{'{{icon}}'}" }
- %span.js-filter-hint
- {{hint}}
- %span.js-filter-tag.dropdown-light-content
- {{tag}}
+ #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
+ %li.filter-dropdown-item{ data: { value: runner_type } }
+ = button_tag class: %w[btn btn-link] do
+ = runner_type.titleize
- #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_STATUSES.each do |status|
- %li.filter-dropdown-item{ data: { value: status } }
- = button_tag class: %w[btn btn-link] do
- = status.titleize
+ = button_tag class: %w[clear-search hidden] do
+ = icon('times')
+ .filter-dropdown-container
+ = render 'sort_dropdown'
- #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
- %li.filter-dropdown-item{ data: { value: runner_type } }
- = button_tag class: %w[btn btn-link] do
- = runner_type.titleize
-
- = button_tag class: %w[clear-search hidden] do
- = icon('times')
- .filter-dropdown-container
- = render 'sort_dropdown'
+ .col-sm-3.text-right-lg
+ = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- if @runners.any?
.runners-content.content-list
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index b1b142460b0..4307060d748 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -13,5 +13,9 @@
= _("Use the following registration token during setup:")
%code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard")
+ .prepend-top-10.append-bottom-10
+ = button_to _("Reset runners registration token"), reset_token_url,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: _("Are you sure you want to reset registration token?") }
%li
= _("Start the Runner!")
diff --git a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml
deleted file mode 100644
index 2a190cb9250..00000000000
--- a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-.bs-callout.help-callout
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: registration_token, type: 'shared' }
diff --git a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml
deleted file mode 100644
index afe57bdfa01..00000000000
--- a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-.bs-callout.help-callout
- .append-bottom-10
- %h4= _('Set up a specific Runner automatically')
-
- %p
- - link_to_help_page = link_to(_('Learn more about Kubernetes'),
- help_page_path('user/project/clusters/index'),
- target: '_blank',
- rel: 'noopener noreferrer')
-
- = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
-
- %ol
- %li
- = _('Click the button below to begin the install process by navigating to the Kubernetes page')
- %li
- = _('Select an existing Kubernetes cluster or create a new one')
- %li
- = _('From the Kubernetes cluster details view, install Runner from the applications list')
-
- = link_to _('Install Runner on Kubernetes'),
- project_clusters_path(@project),
- class: 'btn btn-info'
- %hr
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: registration_token, type: 'specific' }
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 003bd25dd06..5b78ce910b8 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -3,29 +3,23 @@
- can_admin_label = can?(current_user, :admin_label, @group)
- issuables = ['issues', 'merge requests']
- search = params[:search]
+- subscribed = params[:subscribed]
+- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
- if can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_group_label_path(@group), class: "btn btn-success"
-- if @labels.exists? || search.present?
+- if labels_or_filters
#promote-label-modal
%div{ class: container_class }
- .top-area.adjust
- .nav-text
- = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
- .nav-controls
- = form_tag group_labels_path(@group), method: :get do
- .input-group
- = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
- %span.input-group-append
- %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
- = icon("search")
- = render 'shared/labels/sort_dropdown'
+ = render 'shared/labels/nav'
.labels-container.prepend-top-5
- if @labels.any?
+ .text-muted
+ = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
.other-labels
%h5= _('Labels')
%ul.content-list.manage-labels-list.js-other-labels
@@ -34,6 +28,9 @@
- elsif search.present?
.nothing-here-block
= _('No labels with such name or description')
+ - elsif subscribed.present?
+ .nothing-here-block
+ = _('You do not have any subscriptions yet')
- else
= render 'shared/empty_states/labels'
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
index e6c089c3494..bcfb6d99716 100644
--- a/app/views/groups/runners/_group_runners.html.haml
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -11,7 +11,9 @@
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
- if can?(current_user, :admin_pipeline, @group)
= render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: @group.runners_token, type: 'group' }
+ locals: { registration_token: @group.runners_token,
+ type: 'group',
+ reset_token_url: reset_registration_token_group_settings_ci_cd_path }
- if @group.runners.empty?
%h4.underlined-title
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 9ed05d6e3d0..261d758622b 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -5,7 +5,14 @@
.user-name.bold
= current_user.name
= current_user.to_reference
+ - if current_user.status
+ .user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
+ = emoji_icon current_user.status.emoji
+ = current_user.status.message_html.html_safe
%li.divider
+ - if can?(current_user, :update_user_status, current_user)
+ %li
+ .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 044b49c12cc..39604611440 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -74,3 +74,6 @@
%span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
+
+- if can?(current_user, :update_user_status, current_user)
+ .js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index fb4fff12027..759d39cf5f5 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -1,5 +1,5 @@
%p.slead
- Should you ever lose your phone, each of these recovery codes can be used one
+ Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one
time each to regain access to your account. Please save them in a safe place, or you
%b will
lose access to your account.
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index cd10b8758f6..94ec0cc5db8 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -6,13 +6,13 @@
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
- Register Two-Factor Authentication App
+ Register Two-Factor Authenticator
%p
- Use an app on your mobile device to enable two-factor authentication (2FA).
+ Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).
.col-lg-8
- if current_user.two_factor_otp_enabled?
%p
- You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication.
+ You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.
%p
If you lose your recovery codes you can generate new ones, invalidating all previous codes.
%div
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 0222bbf7338..eaf3a93bd15 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -61,15 +61,14 @@
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
- - if rbac_clusters_feature_enabled?
- .form-group
- .form-check
- = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
- = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
- = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
+ .form-group
+ .form-check
+ = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
+ = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index be84f2ae67c..779c9c245c1 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -37,14 +37,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- - if rbac_clusters_feature_enabled?
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ .form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index f497f5b606c..56551ed4d65 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -25,15 +25,14 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- - if rbac_clusters_feature_enabled?
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
- = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
+ .form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 56b597d295a..5b57f7ceb7d 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -26,14 +26,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- - if rbac_clusters_feature_enabled?
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ .form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index c7890b37381..8c5b6e089ea 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -49,15 +49,16 @@
.environments-container
- if @deployments.blank?
- .blank-state-row
- .blank-state-center
- %h2.blank-state-title
+ .empty-state
+ .text-content
+ %h4.state-title
You don't have any deployments right now.
%p.blank-state-text
Define environments in the deploy stage(s) in
%code .gitlab-ci.yml
to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ .text-center
+ = link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
deleted file mode 100644
index ea552c73c92..00000000000
--- a/app/views/projects/jobs/_empty_state.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- illustration = local_assigns.fetch(:illustration)
-- illustration_size = local_assigns.fetch(:illustration_size)
-- title = local_assigns.fetch(:title)
-- content = local_assigns.fetch(:content, nil)
-- action = local_assigns.fetch(:action, nil)
-
-.row.empty-state
- .col-12
- .svg-content{ class: illustration_size }
- = image_tag illustration
- .col-12
- .text-content
- %h4.text-center= title
- - if content
- %p= content
- - if action
- .text-center
- = action
diff --git a/app/views/projects/jobs/_empty_states.html.haml b/app/views/projects/jobs/_empty_states.html.haml
deleted file mode 100644
index e5198d047df..00000000000
--- a/app/views/projects/jobs/_empty_states.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- detailed_status = @build.detailed_status(current_user)
-- illustration = detailed_status.illustration
-
-= render 'empty_state',
- illustration: illustration[:image],
- illustration_size: illustration[:size],
- title: illustration[:title],
- content: illustration[:content],
- action: detailed_status.has_action? ? link_to(detailed_status.action_button_title, detailed_status.action_path, method: detailed_status.action_method, class: 'btn btn-primary', title: detailed_status.action_button_title) : nil
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index ab7963737ca..a5f814b722d 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -42,8 +42,6 @@
= custom_icon('scroll_down')
= render 'shared/builds/build_output'
- - else
- = render "empty_states"
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 683dda4f166..11a05eada30 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -2,32 +2,25 @@
- page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @project)
- search = params[:search]
+- subscribed = params[:subscribed]
+- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
- if can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success"
-- if @labels.exists? || @prioritized_labels.exists? || search.present?
+- if labels_or_filters
#promote-label-modal
%div{ class: container_class }
- .top-area.adjust
- .nav-text
- = _('Labels can be applied to issues and merge requests.')
-
- .nav-controls
- = form_tag project_labels_path(@project), method: :get do
- .input-group
- = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
- %span.input-group-append
- %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
- = icon("search")
- = render 'shared/labels/sort_dropdown'
+ = render 'shared/labels/nav'
.labels-container.prepend-top-10
- if can_admin_label
- if search.blank?
%p.text-muted
+ = _('Labels can be applied to issues and merge requests.')
+ %br
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
@@ -59,7 +52,9 @@
- else
.nothing-here-block
= _('No labels with such name or description')
-
+ - elsif subscribed.present?
+ .nothing-here-block
+ = _('You do not have any subscriptions yet')
- else
= render 'shared/empty_states/labels'
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 314af44490e..ec503cd8bef 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,8 +1,34 @@
%h3
= _('Specific Runners')
-= render partial: 'ci/runner/how_to_setup_specific_runner',
- locals: { registration_token: @project.runners_token }
+.bs-callout.help-callout
+ .append-bottom-10
+ %h4= _('Set up a specific Runner automatically')
+
+ %p
+ - link_to_help_page = link_to(_('Learn more about Kubernetes'),
+ help_page_path('user/project/clusters/index'),
+ target: '_blank',
+ rel: 'noopener noreferrer')
+
+ = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
+
+ %ol
+ %li
+ = _('Click the button below to begin the install process by navigating to the Kubernetes page')
+ %li
+ = _('Select an existing Kubernetes cluster or create a new one')
+ %li
+ = _('From the Kubernetes cluster details view, install Runner from the applications list')
+
+ = link_to _('Install Runner on Kubernetes'),
+ project_clusters_path(@project),
+ class: 'btn btn-info'
+ %hr
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @project.runners_token,
+ type: 'specific',
+ reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index ae923d8e6dc..41afaa9ffc0 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -3,16 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f|
= form_errors(@project)
%fieldset.builds-feature
- .form-group.append-bottom-default.js-secret-runner-token
- = f.label :runners_token, _("Runner token"), class: 'label-bold'
- .form-control.js-secret-value-placeholder
- = '*' * 20
- = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
- %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
- = _('Reveal value')
-
- %hr
.form-group
%h5.prepend-top-0
= _("Git strategy for pipelines")
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 16961784e00..98e2829ba43 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -12,7 +12,7 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.")
+ = _("Customize your pipeline configuration, view your pipeline status and coverage report.")
.settings-content
= render 'form'
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 28e6fe1b16d..0d2f6bb77d6 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -33,7 +33,7 @@
- if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path,
- "label-path" => labels_filter_path,
+ "label-path" => labels_filter_path_with_defaults,
"empty-state-svg" => image_path('illustrations/issues.svg'),
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 532045f3697..6138914206b 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -25,7 +25,7 @@
show_no: "true",
show_any: "true",
project_id: @project&.try(:id),
- labels: labels_filter_path(false),
+ labels: labels_filter_path_with_defaults,
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 0b42b33581a..6eb1f8f0853 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -8,7 +8,7 @@
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 32b609eed0d..aa136af1955 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -109,7 +109,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path_with_defaults if @project), display: 'static' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
new file mode 100644
index 00000000000..98572db738b
--- /dev/null
+++ b/app/views/shared/labels/_nav.html.haml
@@ -0,0 +1,20 @@
+- subscribed = params[:subscribed]
+
+.top-area.adjust
+ %ul.nav-links.nav.nav-tabs
+ %li{ class: active_when(subscribed != 'true') }>
+ = link_to labels_filter_path do
+ = _('All')
+ - if current_user
+ %li{ class: active_when(subscribed == 'true') }>
+ = link_to labels_filter_path(subscribed: 'true') do
+ = _('Subscribed')
+ .nav-controls
+ = form_tag labels_filter_path, method: :get do
+ = hidden_field_tag :subscribed, params[:subscribed]
+ .input-group
+ = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ %span.input-group-append
+ %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
+ = icon("search")
+ = render 'shared/labels/sort_dropdown'
diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml
index ff6e2947ffd..8a7d037e15b 100644
--- a/app/views/shared/labels/_sort_dropdown.html.haml
+++ b/app/views/shared/labels/_sort_dropdown.html.haml
@@ -6,4 +6,4 @@
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
- label_sort_options_hash.each do |value, title|
- = sortable_item(title, page_filter_path(sort: value, label: true), sort_title)
+ = sortable_item(title, page_filter_path(sort: value, label: true, subscribed: params[:subscribed]), sort_title)
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
new file mode 100644
index 00000000000..f8b3754840d
--- /dev/null
+++ b/app/views/users/_overview.html.haml
@@ -0,0 +1,32 @@
+.row
+ .col-md-12.col-lg-6
+ .calendar-block
+ .content-block.hide-bottom-border
+ %h4
+ = s_('UserProfile|Activity')
+ .user-calendar.d-none.d-sm-block.text-left{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities.d-none.d-sm-block
+
+ - if can?(current_user, :read_cross_project)
+ .activities-block
+ .content-block
+ %h5.prepend-top-10
+ = s_('UserProfile|Recent contributions')
+ .overview-content-list{ data: { href: user_path } }
+ .center.light.loading
+ %i.fa.fa-spinner.fa-spin
+ .prepend-top-10
+ = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
+
+ .col-md-12.col-lg-6
+ .projects-block
+ .content-block
+ %h4
+ = s_('UserProfile|Personal projects')
+ .overview-content-list{ data: { href: user_projects_path } }
+ .center.light.loading
+ %i.fa.fa-spinner.fa-spin
+ .prepend-top-10
+ = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 7a38d290915..d6c8420b744 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -12,22 +12,22 @@
.cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
+ = link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
+ %button.btn.btn-danger{ title: s_('UserProfile|Already reported for abuse'),
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
- title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- if can?(current_user, :read_user_profile, @user)
- = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
- = link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area',
+ = link_to [:admin, @user], class: 'btn btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
@@ -51,7 +51,7 @@
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
%span.middle-dot-divider
- Member since #{@user.created_at.to_date.to_s(:long)}
+ = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) }
.cover-desc
- unless @user.public_email.blank?
@@ -91,32 +91,40 @@
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
+ - if profile_tab?(:overview)
+ %li.js-overview-tab
+ = link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do
+ = s_('UserProfile|Overview')
- if profile_tab?(:activity)
%li.js-activity-tab
- = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
- Activity
+ = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
+ = s_('UserProfile|Activity')
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
- Groups
+ = s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
- Contributed projects
+ = s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
- Personal projects
+ = s_('UserProfile|Personal projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
- Snippets
+ = s_('UserProfile|Snippets')
%div{ class: container_class }
.tab-content
+ - if profile_tab?(:overview)
+ #js-overview.tab-pane
+ = render "users/overview"
+
- if profile_tab?(:activity)
#activity.tab-pane
- .row-content-block.calender-block.white.second-block.d-none.d-sm-block
+ .row-content-block.calendar-block.white.second-block.d-none.d-sm-block
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
@@ -124,7 +132,7 @@
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
- Most Recent Activity
+ = s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_path } }
= spinner
@@ -155,4 +163,4 @@
.col-12.text-center
.text-content
%h4
- This user has a private profile
+ = s_('UserProfile|This user has a private profile')