Commit af2c51d7 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master-ce' into scheduled-manual-jobs

parents acfe7ec6 f71c497f
......@@ -606,7 +606,7 @@ static-analysis:
docs lint:
<<: *dedicated-runner
<<: *except-qa
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-docs-lint"
stage: test
cache: {}
dependencies: []
......@@ -614,8 +614,8 @@ docs lint:
script:
- scripts/lint-doc.sh
- scripts/lint-changelog-yaml
- mv doc/ /nanoc/content/
- cd /nanoc
- mv doc/ /tmp/gitlab-docs/content/
- cd /tmp/gitlab-docs
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
......
......@@ -10,24 +10,6 @@
Capybara/CurrentPathExpectation:
Enabled: false
# Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
- 'spec/factories/broadcast_messages.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/runners.rb'
- 'spec/factories/clusters/applications/helm.rb'
- 'spec/factories/clusters/platforms/kubernetes.rb'
- 'spec/factories/emails.rb'
- 'spec/factories/gpg_keys.rb'
- 'spec/factories/group_members.rb'
- 'spec/factories/merge_requests.rb'
- 'spec/factories/notes.rb'
- 'spec/factories/oauth_access_grants.rb'
- 'spec/factories/project_members.rb'
- 'spec/factories/todos.rb'
- 'spec/factories/uploads.rb'
# Offense count: 167
# Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments:
......@@ -53,20 +35,6 @@ Layout/IndentArray:
Layout/IndentHash:
Enabled: false
# Offense count: 11
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceBeforeFirstArg:
Exclude:
- 'config/routes/project.rb'
- 'db/migrate/20170506185517_add_foreign_key_pipeline_schedules_and_pipelines.rb'
- 'features/steps/project/source/browse_files.rb'
- 'features/steps/project/source/markdown_render.rb'
- 'lib/api/runners.rb'
- 'spec/features/search/user_uses_search_filters_spec.rb'
- 'spec/routing/project_routing_spec.rb'
- 'spec/services/system_note_service_spec.rb'
# Offense count: 93
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
......@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg:
Layout/SpaceInLambdaLiteral:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'spec/lib/gitlab/import_export/relation_factory_spec.rb'
# Offense count: 327
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
......@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces:
Layout/SpaceInsideParens:
Enabled: false
# Offense count: 14
# Cop supports --auto-correct.
Layout/SpaceInsidePercentLiteralDelimiters:
Exclude:
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/health_checks/fs_shards_check.rb'
- 'spec/lib/gitlab/health_checks/fs_shards_check_spec.rb'
# Offense count: 26
Lint/DuplicateMethods:
Exclude:
......@@ -135,31 +86,11 @@ Lint/InterpolationCheck:
Lint/MissingCopEnableDirective:
Enabled: false
# Offense count: 2
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
Exclude:
- 'app/models/project.rb'
# Offense count: 1
# Configuration parameters: IgnoreImplicitReferences.
Lint/ShadowedArgument:
Exclude:
- 'lib/gitlab/database/sha_attribute.rb'
# Offense count: 3
# Cop supports --auto-correct.
Lint/UnneededRequireStatement:
Exclude:
- 'db/post_migrate/20161221153951_rename_reserved_project_names.rb'
- 'db/post_migrate/20170313133418_rename_more_reserved_project_names.rb'
- 'lib/declarative_policy.rb'
# Offense count: 9
Lint/UriEscapeUnescape:
Exclude:
......@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming:
Enabled: false
# Offense count: 1
Performance/UnfreezeString:
Exclude:
- 'features/steps/project/commits/commits.rb'
# Offense count: 1
# Cop supports --auto-correct.
Performance/UriDefaultParser:
Exclude:
- 'lib/gitlab/url_sanitizer.rb'
# Offense count: 3821
# Configuration parameters: Prefixes.
......
......@@ -2,6 +2,10 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.3.3 (2018-10-04)
- No changes.
## 11.3.2 (2018-10-03)
### Fixed (4 changes)
......
......@@ -295,6 +295,7 @@ gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
gem 'gitlab-sidekiq-fetcher', require: 'sidekiq-reliable-fetch'
# Metrics
group :metrics do
......
......@@ -301,6 +301,8 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5)
gitlab-styles (2.4.1)
rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
......@@ -1031,6 +1033,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
......
......@@ -304,6 +304,8 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5)
gitlab-styles (2.4.1)
rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
......@@ -1040,6 +1042,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
......
......@@ -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);
......
......@@ -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 => {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents();
return awardsHandler;
},
);
});
}
return awardsHandlerPromise;
}
......@@ -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"
......
......@@ -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);
}
......
......@@ -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>
......
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,
},
});
},
});
}
});
......@@ -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">
......
......@@ -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>
......
......@@ -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 -->
......
......@@ -29,6 +29,16 @@ 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;
......@@ -36,7 +46,8 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* @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 () => {};
......@@ -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 &&
......
......@@ -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) {
......
......@@ -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.'));
});
......@@ -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('');
}
}
}
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');
}
}
......@@ -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 = `
const CALENDAR_TEMPLATES = {
activity: `
<div class="clearfix calendar">
<div class="js-contrib-calendar"></div>
<div class="calendar-hint">
Summary of issues, merge requests, push events, and comments
<div class="calendar-hint bottom-right"></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) {
......
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;
import Vue from 'vue';
export default new Vue();
<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>
<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>
......@@ -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
......
.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;
&.bottom-right {
direction: ltr;
margin-top: -23px;
float: right;
}
}
.pika-single.gitlab-theme {
......
......@@ -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;
}
}
......@@ -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;
}
}
}
......@@ -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;
......@@ -18,3 +18,4 @@ $success: $green-500;
$info: $blue-500;
$warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
......@@ -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 {
......
......@@ -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;
......
......@@ -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;
}
......
@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;
......
......@@ -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
......
# 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
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
......@@ -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)
......
......@@ -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))
......
......@@ -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
......
......@@ -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,
......
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -633,6 +633,10 @@ module Ci
end
end
def default_branch?
ref == project.default_branch
end
private
def ci_yaml_from_repo
......
......@@ -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)
......
......@@ -31,9 +31,11 @@ module Subscribable
end
def subscribers(project)
subscriptions_available(project)
relation = subscriptions_available(project)
.where(subscribed: true)
.map(&:user)
.select(:user_id)
User.where(id: relation)
end
def toggle_subscription(user, project = nil)
......
......@@ -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
......@@ -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
......
......@@ -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.
......
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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
......@@ -210,8 +210,13 @@ 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
project &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
end
......@@ -286,6 +291,7 @@ module QuickActions
end
params '#issue | !merge_request'
condition do
[MergeRequest, Issue].include?(issuable.class) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
......@@ -443,6 +449,7 @@ module QuickActions
end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
issuable.is_a?(TimeTrackable) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |raw_time_date|
......@@ -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)
......
- 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
......
......@@ -2,6 +2,8 @@
- @no_container = true
%div{ class: container_class }
.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.")
......@@ -33,23 +35,15 @@
\-
= _('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?') }
= render partial: 'ci/runner/how_to_setup_shared_runner',
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
.col-sm-6
.bs-callout
%p
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
= 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 }
.row-content-block.second-block
.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
......@@ -103,6 +97,9 @@
.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
.table-holder
......
......@@ -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!")
.bs-callout.help-callout
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'shared' }
.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' }
......@@ -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'
......
......@@ -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
......
......@@ -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 }
......
......@@ -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 : '' } }
%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.
......
......@@ -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
......
......@@ -61,7 +61,6 @@
%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
......
......@@ -37,7 +37,6 @@
= 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'
......
......@@ -25,10 +25,9 @@
= 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.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).')
......
......@@ -26,7 +26,6 @@
= 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'
......
......@@ -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' }
......
- 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
- 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
......@@ -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 } }
......
......@@ -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'
......
%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
......
......@@ -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")
......
......@@ -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'
......
......@@ -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",
......
......@@ -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
......
......@@ -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)
......
......@@ -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')
......
- 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'
......@@ -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)
.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"
......@@ -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')
---
title: Show percentage of language detection on the language bar
merge_request: 22056
author: Johann Hubert Sonntagbauer
type: added
---
title: "Fix the issue where long environment names aren't being truncated, causing the environment name to overlap into the column next to it."
merge_request: 22104
type: fixed
---
title: Rephrase 2FA and TOTP documentation and view
merge_request: 21998
author: Marc Schwede
type: other
---
title: Instance Configuration page now displays correct SSH fingerprints
merge_request: 22081
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment