Commit d9a8d9f3 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'backstage/gb/build-stages-catch-up-migration'

 Conflicts:
   db/schema.rb
parents 6cb5b7c8 02d9f54f
...@@ -10,15 +10,26 @@ AllCops: ...@@ -10,15 +10,26 @@ AllCops:
Exclude: Exclude:
- 'vendor/**/*' - 'vendor/**/*'
- 'node_modules/**/*' - 'node_modules/**/*'
- 'db/*' - 'db/**/*'
- 'db/fixtures/**/*' - 'db/fixtures/**/*'
- 'db/geo/*' - 'ee/db/**/*'
- 'tmp/**/*' - 'tmp/**/*'
- 'bin/**/*' - 'bin/**/*'
- 'generator_templates/**/*' - 'generator_templates/**/*'
- 'builds/**/*' - 'builds/**/*'
CacheRootDirectory: tmp CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a
# mutable literal (e.g. array or hash).
Style/MutableConstant:
Enabled: true
Exclude:
- 'db/migrate/**/*'
- 'db/post_migrate/**/*'
- 'ee/db/migrate/**/*'
- 'ee/db/post_migrate/**/*'
- 'ee/db/geo/migrate/**/*'
# Gitlab ################################################################### # Gitlab ###################################################################
Gitlab/ModuleWithInstanceVariables: Gitlab/ModuleWithInstanceVariables:
...@@ -33,3 +44,16 @@ Gitlab/ModuleWithInstanceVariables: ...@@ -33,3 +44,16 @@ Gitlab/ModuleWithInstanceVariables:
# We ignore spec helpers because it usually doesn't matter # We ignore spec helpers because it usually doesn't matter
- spec/support/**/*.rb - spec/support/**/*.rb
- features/steps/**/*.rb - features/steps/**/*.rb
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
- 'config/**/*'
- 'db/**/*'
- 'features/**/*'
- 'lib/**/*.rake'
- 'qa/**/*'
- 'spec/**/*'
- 'ee/db/**/*'
- 'ee/lib/**/*.rake'
- 'ee/spec/**/*'
...@@ -174,7 +174,7 @@ Assigning a team label makes sure issues get the attention of the appropriate ...@@ -174,7 +174,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people. people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge, The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
~Geo, ~Gitaly, ~Platform, ~Monitoring, ~Release, and ~"UX". ~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team. responsibility of each team.
......
...@@ -71,11 +71,15 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -71,11 +71,15 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature freeze on the 7th for the release on the 22nd ## Feature freeze on the 7th for the release on the 22nd
After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. After 7th at 23:59 (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period, Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
Any release candidate that gets created after this date can become a final release,
hence the name release candidate.
### Between the 1st and the 7th ### Between the 1st and the 7th
These types of merge requests for the upcoming release need special consideration: These types of merge requests for the upcoming release need special consideration:
...@@ -193,11 +197,10 @@ to be backported down to the `9.5` release, you will need to assign it the ...@@ -193,11 +197,10 @@ to be backported down to the `9.5` release, you will need to assign it the
### Asking for an exception ### Asking for an exception
If you think a merge request should go into an RC or patch even though it does not meet these requirements, If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer: you can ask for an exception to be made.
1. a Release Manager Go to [Release tasks issue tracker](https://gitlab.com/gitlab-org/release/tasks/issues/new) and create an issue
2. an Engineering Lead using the `Exception-request` issue template.
3. an Engineering Director, the VP of Engineering, or the CTO
You can find who is who on the [team page](https://about.gitlab.com/team/). You can find who is who on the [team page](https://about.gitlab.com/team/).
......
...@@ -50,10 +50,8 @@ class AwardsHandler { ...@@ -50,10 +50,8 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', (e) => { this.registerEventListener('on', $('html'), 'click', (e) => {
const $target = $(e.target); const $target = $(e.target);
if (!$target.closest('.emoji-menu-content').length) {
$('.js-awards-block.current').removeClass('current');
}
if (!$target.closest('.emoji-menu').length) { if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) { if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active'); $('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu')); this.hideMenuElement($('.emoji-menu'));
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../flash'; import Flash from '../../flash';
import { __ } from '../../locale';
import Sidebar from '../../right_sidebar'; import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
...@@ -95,7 +96,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -95,7 +96,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}) })
.catch(() => { .catch(() => {
this.loadingAssignees = false; this.loadingAssignees = false;
return new Flash('An error occurred while saving assignees'); Flash(__('An error occurred while saving assignees'));
}); });
}, },
}, },
......
/* eslint-disable no-new */
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale';
import './lists_dropdown'; import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility'; import { pluralize } from '../../../lib/utils/text_utility';
...@@ -36,7 +35,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -36,7 +35,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
gl.boardService.bulkUpdate(issueIds, { gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id], add_label_ids: [list.label.id],
}).catch(() => { }).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert'); Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => { selectedIssues.forEach((issue) => {
list.removeIssue(issue); list.removeIssue(issue);
......
/* eslint-disable no-new */
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -45,7 +44,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -45,7 +44,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
}; };
Vue.http.patch(this.updateUrl, data).catch(() => { Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert'); Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach((list) => { lists.forEach((list) => {
list.addIssue(issue); list.addIssue(issue);
......
...@@ -180,6 +180,7 @@ export default class CreateMergeRequestDropdown { ...@@ -180,6 +180,7 @@ export default class CreateMergeRequestDropdown {
valueAttribute: 'data-text', valueAttribute: 'data-text',
}, },
], ],
hideOnClick: false,
}; };
} }
......
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff'; import SingleFileDiff from './single_file_diff';
...@@ -69,7 +72,9 @@ export default class Diff { ...@@ -69,7 +72,9 @@ export default class Diff {
const view = file.data('view'); const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view }; const params = { since, to, bottom, offset, unfold, view };
$.get(link, params, response => $target.parent().replaceWith(response)); axios.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() => flash(__('An error occurred while loading diff')));
} }
openAnchoredDiff(cb) { openAnchoredDiff(cb) {
......
...@@ -3,7 +3,6 @@ const DATA_DROPDOWN = 'data-dropdown'; ...@@ -3,7 +3,6 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected'; const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active'; const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore'; const IGNORE_CLASS = 'droplab-item-ignore';
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`. // Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
...@@ -14,5 +13,4 @@ export { ...@@ -14,5 +13,4 @@ export {
ACTIVE_CLASS, ACTIVE_CLASS,
TEMPLATE_REGEX, TEMPLATE_REGEX,
IGNORE_CLASS, IGNORE_CLASS,
IGNORE_HIDING_CLASS,
}; };
import utils from './utils'; import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants'; import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
class DropDown { class DropDown {
constructor(list, config = {}) { constructor(list, config = { }) {
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = true; this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list; this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = []; this.items = [];
this.eventWrapper = {}; this.eventWrapper = {};
this.hideOnClick = config.hideOnClick !== false;
if (config.addActiveClassToDropdownButton) { if (config.addActiveClassToDropdownButton) {
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle'); this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
...@@ -37,15 +38,17 @@ class DropDown { ...@@ -37,15 +38,17 @@ class DropDown {
clickEvent(e) { clickEvent(e) {
if (e.target.tagName === 'UL') return; if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return; if (e.target.closest(`.${IGNORE_CLASS}`)) return;
const selected = utils.closest(e.target, 'LI'); const selected = e.target.closest('li');
if (!selected) return; if (!selected) return;
this.addSelectedClass(selected); this.addSelectedClass(selected);
e.preventDefault(); e.preventDefault();
if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide(); if (this.hideOnClick) {
this.hide();
}
const listEvent = new CustomEvent('click.dl', { const listEvent = new CustomEvent('click.dl', {
detail: { detail: {
......
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge'); const badges = $('.js-loading-gpg-badge');
...@@ -5,13 +10,13 @@ export default class GpgBadges { ...@@ -5,13 +10,13 @@ export default class GpgBadges {
badges.html('<i class="fa fa-spinner fa-spin"></i>'); badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({ const params = parseQueryStringIntoObject(form.serialize());
url: form.data('signatures-path'), return axios.get(form.data('signatures-path'), { params })
data: form.serialize(), .then(({ data }) => {
}).done((response) => { data.signatures.forEach((signature) => {
response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
}); });
}); })
.catch(() => flash(__('An error occurred while loading commits')));
} }
} }
import axios from './lib/utils/axios_utils';
import Api from './api'; import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils'; import { normalizeHeaders } from './lib/utils/common_utils';
export default function groupsSelect() { export default function groupsSelect() {
// Needs to be accessible in rspec // Needs to be accessible in rspec
...@@ -17,24 +18,23 @@ export default function groupsSelect() { ...@@ -17,24 +18,23 @@ export default function groupsSelect() {
dataType: 'json', dataType: 'json',
quietMillis: 250, quietMillis: 250,
transport(params) { transport(params) {
return $.ajax(params) axios[params.type.toLowerCase()](params.url, {
.then((data, status, xhr) => { params: params.data,
const results = data || []; })
.then((res) => {
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0; const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages; const more = currentPage < totalPages;
return { params.success({
results, results,
pagination: { pagination: {
more, more,
}, },
}; });
}) }).catch(params.error);
.then(params.success)
.fail(params.error);
}, },
data(search, page) { data(search, page) {
return { return {
......
...@@ -24,32 +24,29 @@ export default class Issue { ...@@ -24,32 +24,29 @@ export default class Issue {
if (Issue.createMrDropdownWrap) { if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap); this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
} }
}
initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
return $(document).on('click', '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', (e) => { // Listen to state changes in the Vue app
var $button, shouldSubmit, url; document.addEventListener('issuable_vue_app:change', (event) => {
e.preventDefault(); this.updateTopState(event.detail.isClosed, event.detail.data);
e.stopImmediatePropagation(); });
$button = $(e.currentTarget);
shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
} }
this.disableCloseReopenButton($button); /**
* This method updates the top area of the issue.
url = $button.attr('href'); *
return axios.put(url) * Once the issue state changes, either through a click on the top area (jquery)
.then(({ data }) => { * or a click on the bottom area (Vue) we need to update the top area.
*
* @param {Boolean} isClosed
* @param {Array} data
* @param {String} issueFailMessage
*/
updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') {
if ('id' in data) {
const isClosedBadge = $('div.status-box-issue-closed'); const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open'); const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter'); const projectIssuesCounter = $('.issue_counter');
if ('id' in data) {
const isClosed = $button.hasClass('btn-close');
isClosedBadge.toggleClass('hidden', !isClosed); isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed); isOpenBadge.toggleClass('hidden', isClosed);
...@@ -72,6 +69,28 @@ export default class Issue { ...@@ -72,6 +69,28 @@ export default class Issue {
} else { } else {
flash(issueFailMessage); flash(issueFailMessage);
} }
}
initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
return $(document).on('click', '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', (e) => {
var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
$button = $(e.currentTarget);
shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
}
this.disableCloseReopenButton($button);
url = $button.attr('href');
return axios.put(url)
.then(({ data }) => {
const isClosed = $button.hasClass('btn-close');
this.updateTopState(isClosed, data);
}) })
.catch(() => flash(issueFailMessage)) .catch(() => flash(issueFailMessage))
.then(() => { .then(() => {
......
...@@ -2,16 +2,18 @@ ...@@ -2,16 +2,18 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import Autosize from 'autosize'; import Autosize from 'autosize';
import { __ } from '~/locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Autosave from '../../autosave'; import Autosave from '../../autosave';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import * as constants from '../constants'; import * as constants from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
export default { export default {
...@@ -22,6 +24,7 @@ ...@@ -22,6 +24,7 @@
discussionLockedWidget, discussionLockedWidget,
markdownField, markdownField,
userAvatarLink, userAvatarLink,
loadingButton,
}, },
mixins: [ mixins: [
issuableStateMixin, issuableStateMixin,
...@@ -30,9 +33,6 @@ ...@@ -30,9 +33,6 @@
return { return {
note: '', note: '',
noteType: constants.COMMENT, noteType: constants.COMMENT,
// Can't use mapGetters,
// this needs to be in the data object because it belongs to the state
issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false, isSubmitting: false,
isSubmitButtonDisabled: true, isSubmitButtonDisabled: true,
}; };
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
'getUserData', 'getUserData',
'getNoteableData', 'getNoteableData',
'getNotesData', 'getNotesData',
'issueState',
]), ]),
isLoggedIn() { isLoggedIn() {
return this.getUserData.id; return this.getUserData.id;
...@@ -105,7 +106,7 @@ ...@@ -105,7 +106,7 @@
mounted() { mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery. // jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => { $(document).on('issuable:change', (e, isClosed) => {
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
}); });
this.initAutoSave(); this.initAutoSave();
...@@ -117,6 +118,9 @@ ...@@ -117,6 +118,9 @@
'stopPolling', 'stopPolling',
'restartPolling', 'restartPolling',
'removePlaceholderNotes', 'removePlaceholderNotes',
'closeIssue',
'reopenIssue',
'toggleIssueLocalState',
]), ]),
setIsSubmitButtonDisabled(note, isSubmitting) { setIsSubmitButtonDisabled(note, isSubmitting) {
if (!_.isEmpty(note) && !isSubmitting) { if (!_.isEmpty(note) && !isSubmitting) {
...@@ -126,6 +130,8 @@ ...@@ -126,6 +130,8 @@
} }
}, },
handleSave(withIssueAction) { handleSave(withIssueAction) {
this.isSubmitting = true;
if (this.note.length) { if (this.note.length) {
const noteData = { const noteData = {
endpoint: this.endpoint, endpoint: this.endpoint,
...@@ -142,7 +148,6 @@ ...@@ -142,7 +148,6 @@
if (this.noteType === constants.DISCUSSION) { if (this.noteType === constants.DISCUSSION) {
noteData.data.note.type = constants.DISCUSSION_NOTE; noteData.data.note.type = constants.DISCUSSION_NOTE;
} }
this.isSubmitting = true;
this.note = ''; // Empty textarea while being requested. Repopulate in catch this.note = ''; // Empty textarea while being requested. Repopulate in catch
this.resizeTextarea(); this.resizeTextarea();
this.stopPolling(); this.stopPolling();
...@@ -184,13 +189,25 @@ Please check your network connection and try again.`; ...@@ -184,13 +189,25 @@ Please check your network connection and try again.`;
this.toggleIssueState(); this.toggleIssueState();
} }
}, },
enableButton() {
this.isSubmitting = false;
},
toggleIssueState() { toggleIssueState() {
this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED; if (this.isIssueOpen) {
this.closeIssue()
// This is out of scope for the Notes Vue component. .then(() => this.enableButton())
// It was the shortest path to update the issue state and relevant places. .catch(() => {
const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; this.enableButton();
$(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); Flash(__('Something went wrong while closing the issue. Please try again later'));
});
} else {
this.reopenIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
Flash(__('Something went wrong while reopening the issue. Please try again later'));
});
}
}, },
discard(shouldClear = true) { discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired. // `blur` is needed to clear slash commands autocomplete cache if event fired.
...@@ -367,15 +384,19 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -367,15 +384,19 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</li> </li>
</ul> </ul>
</div> </div>
<button
type="button" <loading-button
@click="handleSave(true)"
v-if="canUpdateIssue" v-if="canUpdateIssue"
:class="actionButtonClassNames" :loading="isSubmitting"
@click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
:disabled="isSubmitting" :disabled="isSubmitting"
class="btn btn-comment btn-comment-and-close js-action-button"> :label="issueActionButtonTitle"
{{ issueActionButtonTitle }} />
</button>
<button <button
type="button" type="button"
v-if="note.length" v-if="note.length"
......
...@@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
notesPath: notesDataset.notesPath, notesPath: notesDataset.notesPath,
markdownDocsPath: notesDataset.markdownDocsPath, markdownDocsPath: notesDataset.markdownDocsPath,
quickActionsDocsPath: notesDataset.quickActionsDocsPath, quickActionsDocsPath: notesDataset.quickActionsDocsPath,
closeIssuePath: notesDataset.closeIssuePath,
reopenIssuePath: notesDataset.reopenIssuePath,
}, },
}; };
}, },
......
...@@ -32,4 +32,7 @@ export default { ...@@ -32,4 +32,7 @@ export default {
toggleAward(endpoint, data) { toggleAward(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true }); return Vue.http.post(endpoint, data, { emulateJSON: true });
}, },
toggleIssueState(endpoint, data) {
return Vue.http.put(endpoint, data);
},
}; };
...@@ -61,6 +61,39 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service ...@@ -61,6 +61,39 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) => export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES); commit(types.REMOVE_PLACEHOLDER_NOTES);
export const closeIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.closeIssuePath)
.then(res => res.json())
.then((data) => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
});
export const reopenIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.reopenIssuePath)
.then(res => res.json())
.then((data) => {
commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data);
});
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
data,
isClosed: getters.issueState === constants.CLOSED,
} });
document.dispatchEvent(event);
};
export const toggleIssueLocalState = ({ commit }, newState) => {
if (newState === constants.CLOSED) {
commit(types.CLOSE_ISSUE);
} else if (newState === constants.REOPENED) {
commit(types.REOPEN_ISSUE);
}
};
export const saveNote = ({ commit, dispatch }, noteData) => { export const saveNote = ({ commit, dispatch }, noteData) => {
const { note } = noteData.data.note; const { note } = noteData.data.note;
let placeholderText = note; let placeholderText = note;
......
...@@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop]; ...@@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData; export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop]; export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const issueState = state => state.noteableData.state;
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
......
...@@ -12,3 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; ...@@ -12,3 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD'; export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_NOTE = 'UPDATE_NOTE';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
...@@ -152,4 +152,12 @@ export default { ...@@ -152,4 +152,12 @@ export default {
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
} }
}, },
[types.CLOSE_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.CLOSED });
},
[types.REOPEN_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.REOPENED });
},
}; };
...@@ -14,7 +14,7 @@ export default () => { ...@@ -14,7 +14,7 @@ export default () => {
$('#tree-slider').waitForImages(() => $('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath));
const commitPipelineStatusEl = document.getElementById('commit-pipeline-status'); const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link'); const statusLink = document.querySelector('.commit-actions .ci-status-link');
if (statusLink != null) { if (statusLink != null) {
statusLink.remove(); statusLink.remove();
......
...@@ -33,9 +33,6 @@ import flash from '../flash'; ...@@ -33,9 +33,6 @@ import flash from '../flash';
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie); $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm); $('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
this.form.on('submit', this.onSubmitForm); this.form.on('submit', this.onSubmitForm);
} }
...@@ -48,21 +45,6 @@ import flash from '../flash'; ...@@ -48,21 +45,6 @@ import flash from '../flash';
return this.saveForm(); return this.saveForm();
} }
beforeUpdateUsername() {
$('.loading-username', this).removeClass('hidden');
}
afterUpdateUsername() {
$('.loading-username', this).addClass('hidden');
$('button[type=submit]', this).enable();
}
onUpdateNotifs(e, data) {
return data.saved ?
flash(__('Notification settings saved'), 'notice') :
flash(__('Failed to save new settings'));
}
saveForm() { saveForm() {
const self = this; const self = this;
const formData = new FormData(this.form[0]); const formData = new FormData(this.form[0]);
......
/* global katex */ import { __ } from './locale';
import flash from './flash';
// Renders math using KaTeX in any element with the // Renders math using KaTeX in any element with the
// `js-render-math` class // `js-render-math` class
...@@ -8,15 +9,8 @@ ...@@ -8,15 +9,8 @@
// <code class="js-render-math"></div> // <code class="js-render-math"></div>
// //
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
// Only load once
let katexLoaded = false;
// Loop over all math elements and render math // Loop over all math elements and render math
function renderWithKaTeX(elements) { function renderWithKaTeX(elements, katex) {
elements.each(function katexElementsLoop() { elements.each(function katexElementsLoop() {
const mathNode = $('<span></span>'); const mathNode = $('<span></span>');
const $this = $(this); const $this = $(this);
...@@ -34,30 +28,10 @@ function renderWithKaTeX(elements) { ...@@ -34,30 +28,10 @@ function renderWithKaTeX(elements) {
export default function renderMath($els) { export default function renderMath($els) {
if (!$els.length) return; if (!$els.length) return;
Promise.all([
if (katexLoaded) { import(/* webpackChunkName: 'katex' */ 'katex'),
renderWithKaTeX($els); import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'),
} else { ]).then(([katex]) => {
axios.get(gon.katex_css_url) renderWithKaTeX($els, katex);
.then(() => { }).catch(() => flash(__('An error occurred while rendering KaTeX')));
const css = $('<link>', {
rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
});
css.appendTo('head');
})
.then(() => axios.get(gon.katex_js_url, {
responseType: 'text',
}))
.then(({ data }) => {
// Add katex js to our document
$.globalEval(data);
})
.then(() => {
katexLoaded = true;
renderWithKaTeX($els); // Run KaTeX
})
.catch(() => flash(__('An error occurred while rendering KaTeX')));
}
} }
...@@ -42,7 +42,7 @@ export default function initSettingsPanels() { ...@@ -42,7 +42,7 @@ export default function initSettingsPanels() {
if (location.hash) { if (location.hash) {
const $target = $(location.hash); const $target = $(location.hash);
if ($target.length && $target.hasClass('.settings')) { if ($target.length && $target.hasClass('settings')) {
expandSection($target); expandSection($target);
} }
} }
......
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button <button
v-if="isEditable" v-if="isEditable"
class="pull-right lock-edit btn btn-blank" class="pull-right lock-edit"
type="button" type="button"
@click.prevent="toggleForm" @click.prevent="toggleForm"
> >
......
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
Ask someone with write access to this repository to merge this request
</span>
</div>
</div>
`,
};
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="success"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|Ready to be merged automatically.
Ask someone with write access to this repository to merge this request`) }}
</span>
</div>
</div>
</template>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
</span>
</div>
</div>
`,
};
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|Pipeline blocked.
The pipeline for this merge request requires a manual action to proceed`) }}
</span>
</div>
</div>
</template>
...@@ -25,11 +25,11 @@ export { default as ArchivedState } from './components/states/mr_widget_archived ...@@ -25,11 +25,11 @@ export { default as ArchivedState } from './components/states/mr_widget_archived
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge'; export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch'; export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
......
...@@ -152,6 +152,7 @@ export default { ...@@ -152,6 +152,7 @@ export default {
}, },
handleNotification(data) { handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return; if (data.ci_status === this.mr.ciStatus) return;
if (!data.pipeline) return;
const label = data.pipeline.details.status.label; const label = data.pipeline.details.status.label;
const title = `Pipeline ${label}`; const title = `Pipeline ${label}`;
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
required: false, required: false,
}, },
containerClass: { containerClass: {
type: String, type: [String, Array, Object],
required: false, required: false,
default: 'btn btn-align-content', default: 'btn btn-align-content',
}, },
......
...@@ -449,9 +449,11 @@ img.emoji { ...@@ -449,9 +449,11 @@ img.emoji {
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; } .prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-16 { margin-top: 16px; }
.prepend-top-20 { margin-top: 20px; } .prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; } .prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; } .prepend-left-5 { margin-left: 5px; }
.prepend-left-8 { margin-left: 8px; }
.prepend-left-10 { margin-left: 10px; } .prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; } .prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; } .prepend-left-20 { margin-left: 20px; }
......
...@@ -736,10 +736,6 @@ ...@@ -736,10 +736,6 @@
} }
} }
.droplab-item-ignore {
pointer-events: none;
}
.pika-single.animate-picker.is-bound, .pika-single.animate-picker.is-bound,
.pika-single.animate-picker.is-bound.is-hidden { .pika-single.animate-picker.is-bound.is-hidden {
/* /*
......
...@@ -182,6 +182,7 @@ label { ...@@ -182,6 +182,7 @@ label {
.help-block { .help-block {
margin-bottom: 0; margin-bottom: 0;
margin-top: #{$grid-size / 2};
} }
.gl-field-error { .gl-field-error {
......
...@@ -197,11 +197,18 @@ ...@@ -197,11 +197,18 @@
margin-left: 0; margin-left: 0;
} }
a.edit-link:not([href]):hover {
color: rgba($avatar-border, .2);
}
.lock-edit, // uses same style, different js behaviour
.edit-link { .edit-link {
@extend .btn-blank;
color: $gl-text-color; color: $gl-text-color;
&:not([href]):hover { &:hover {
color: rgba($avatar-border, .2); text-decoration: underline;
color: $md-link-color;
} }
} }
} }
......
...@@ -181,11 +181,6 @@ ul.related-merge-requests > li { ...@@ -181,11 +181,6 @@ ul.related-merge-requests > li {
} }
.create-mr-dropdown-wrap { .create-mr-dropdown-wrap {
.branch-message,
.ref-message {
display: none;
}
.ref::selection { .ref::selection {
color: $placeholder-text-color; color: $placeholder-text-color;
} }
...@@ -216,6 +211,17 @@ ul.related-merge-requests > li { ...@@ -216,6 +211,17 @@ ul.related-merge-requests > li {
transform: translateY(0); transform: translateY(0);
display: none; display: none;
margin-top: 4px; margin-top: 4px;
// override dropdown item styles
.btn.btn-success {
@include btn-default;
@include btn-green;
border-style: solid;
border-width: 1px;
line-height: $line-height-base;
width: auto;
}
} }
.create-merge-request-dropdown-toggle { .create-merge-request-dropdown-toggle {
...@@ -225,66 +231,6 @@ ul.related-merge-requests > li { ...@@ -225,66 +231,6 @@ ul.related-merge-requests > li {
margin-left: 0; margin-left: 0;
} }
} }
.droplab-item-ignore {
pointer-events: auto;
}
.create-item {
cursor: pointer;
margin: 0 1px;
&:hover,
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
}
li.divider {
margin: 8px 10px;
}
li:not(.divider) {
padding: 8px 9px;
&:last-child {
padding-bottom: 8px;
}
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
.description {
display: block;
}
}
&.droplab-item-ignore {
padding-top: 8px;
}
.icon-container {
float: left;
i {
visibility: hidden;
}
}
.description {
padding-left: 22px;
}
input,
span {
margin: 4px 0 0;
}
}
} }
.discussion-reply-holder .note-edit-form { .discussion-reply-holder .note-edit-form {
......
...@@ -39,12 +39,12 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -39,12 +39,12 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
def verify_billing def verify_billing
case google_project_billing_status case google_project_billing_status
when 'true' when nil
return
when 'false'
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
when false
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
when true
return
end end
@cluster = ::Clusters::Cluster.new(create_params) @cluster = ::Clusters::Cluster.new(create_params)
...@@ -81,9 +81,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -81,9 +81,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
def google_project_billing_status def google_project_billing_status
Gitlab::Redis::SharedState.with do |redis| CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
redis.get(CheckGcpProjectBillingWorker.redis_shared_state_key_for(token_in_session))
end
end end
def token_in_session def token_in_session
......
...@@ -122,8 +122,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -122,8 +122,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def referenced_merge_requests def referenced_merge_requests
@merge_requests = @issue.referenced_merge_requests(current_user) @merge_requests, @closed_by_merge_requests = ::Issues::FetchReferencedMergeRequestsService.new(project, current_user).execute(issue)
@closed_by_merge_requests = @issue.closed_by_merge_requests(current_user)
respond_to do |format| respond_to do |format|
format.json do format.json do
......
...@@ -21,7 +21,7 @@ class IssuesFinder < IssuableFinder ...@@ -21,7 +21,7 @@ class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
def klass def klass
Issue Issue.includes(:author)
end end
def with_confidentiality_access_check def with_confidentiality_access_check
......
...@@ -57,7 +57,7 @@ class NotesFinder ...@@ -57,7 +57,7 @@ class NotesFinder
types = %w(commit issue merge_request snippet) types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) } note_relations = types.map { |t| notes_for_type(t) }
note_relations.map! { |notes| search(notes) } note_relations.map! { |notes| search(notes) }
UnionFinder.new.find_union(note_relations, Note) UnionFinder.new.find_union(note_relations, Note.includes(:author))
end end
def noteables_for_type(noteable_type) def noteables_for_type(noteable_type)
......
...@@ -68,18 +68,32 @@ module ApplicationHelper ...@@ -68,18 +68,32 @@ module ApplicationHelper
end end
end end
def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true) # Takes both user and email and returns the avatar_icon by
user = # user (preferred) or email.
if user_or_email.is_a?(User) def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
user_or_email if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
elsif email
avatar_icon_for_email(email, size, scale, only_path: only_path)
else
default_avatar
end
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
user = User.find_by_any_email(email.try(:downcase))
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else else
User.find_by_any_email(user_or_email.try(:downcase)) gravatar_icon(email, size, scale)
end
end end
def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
if user if user
user.avatar_url(size: size, only_path: only_path) || default_avatar user.avatar_url(size: size, only_path: only_path) || default_avatar
else else
gravatar_icon(user_or_email, size, scale) gravatar_icon(nil, size, scale)
end end
end end
......
...@@ -8,10 +8,22 @@ module AvatarsHelper ...@@ -8,10 +8,22 @@ module AvatarsHelper
})) }))
end end
def user_avatar_url_for(options = {})
if options[:url]
options[:url]
elsif options[:user]
avatar_icon_for_user(options[:user], options[:size])
else
avatar_icon_for_email(options[:user_email], options[:size])
end
end
def user_avatar_without_link(options = {}) def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16 avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
avatar_url = user_avatar_url_for(options.merge(size: avatar_size))
has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip] has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
data_attributes = options[:data] || {} data_attributes = options[:data] || {}
css_class = %W[avatar s#{avatar_size}].push(*options[:css_class]) css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
......
...@@ -160,7 +160,7 @@ module DiffHelper ...@@ -160,7 +160,7 @@ module DiffHelper
end end
def diff_file_changed_icon(diff_file) def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file? if diff_file.deleted_file?
"file-deletion" "file-deletion"
elsif diff_file.new_file? elsif diff_file.new_file?
"file-addition" "file-addition"
......
...@@ -33,7 +33,7 @@ module NamespacesHelper ...@@ -33,7 +33,7 @@ module NamespacesHelper
if namespace.is_a?(Group) if namespace.is_a?(Group)
group_icon(namespace) group_icon(namespace)
else else
avatar_icon(namespace.owner.email, size) avatar_icon_for_user(namespace.owner, size)
end end
end end
......
...@@ -19,7 +19,7 @@ module ProjectsHelper ...@@ -19,7 +19,7 @@ module ProjectsHelper
classes = %W[avatar avatar-inline s#{opts[:size]}] classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class] classes << opts[:avatar_class] if opts[:avatar_class]
avatar = avatar_icon(author, opts[:size]) avatar = avatar_icon_for_user(author, opts[:size])
src = opts[:lazy_load] ? nil : avatar src = opts[:lazy_load] ? nil : avatar
image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar) image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
...@@ -296,6 +296,10 @@ module ProjectsHelper ...@@ -296,6 +296,10 @@ module ProjectsHelper
nav_tabs << :pipelines nav_tabs << :pipelines
end end
if project.external_issue_tracker
nav_tabs << :external_issue_tracker
end
tab_ability_map.each do |tab, ability| tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project) if can?(current_user, ability, project)
nav_tabs << tab nav_tabs << tab
......
...@@ -41,12 +41,41 @@ module Ci ...@@ -41,12 +41,41 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
# This convoluted mess is because we need to handle two cases of
# artifact files during the migration. And a simple OR clause
# makes it impossible to optimize.
# Instead we want to use UNION ALL and do two carefully
# constructed disjoint queries. But Rails cannot handle UNION or
# UNION ALL queries so we do the query in a subquery and wrap it
# in an otherwise redundant WHERE IN query (IN is fine for
# non-null columns).
# This should all be ripped out when the migration is finished and
# replaced with just the new storage to avoid the extra work.
scope :with_artifacts, ->() do scope :with_artifacts, ->() do
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', old = Ci::Build.select(:id).where(%q[artifacts_file <> ''])
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id')) new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id'))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end end
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_artifacts_not_expired, ->() do
old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND (artifacts_expire_at IS NULL OR artifacts_expire_at > ?)], Time.now)
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND (expire_at IS NULL OR expire_at > ?)', Time.now))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :with_expired_artifacts, ->() do
old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND artifacts_expire_at < ?], Time.now)
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND expire_at < ?', Time.now))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) } scope :ref_protected, -> { where(protected: true) }
......
...@@ -116,6 +116,10 @@ class Commit ...@@ -116,6 +116,10 @@ class Commit
raw.id raw.id
end end
def project_id
project.id
end
def ==(other) def ==(other)
other.is_a?(self.class) && raw == other.raw other.is_a?(self.class) && raw == other.raw
end end
......
...@@ -96,10 +96,6 @@ class Event < ActiveRecord::Base ...@@ -96,10 +96,6 @@ class Event < ActiveRecord::Base
self.inheritance_column = 'action' self.inheritance_column = 'action'
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
# include this column, hence we're ignoring it as well.
ignore_column :data
class << self class << self
def model_name def model_name
ActiveModel::Name.new(self, nil, 'event') ActiveModel::Name.new(self, nil, 'event')
......
...@@ -39,7 +39,7 @@ class ExternalIssue ...@@ -39,7 +39,7 @@ class ExternalIssue
end end
def to_reference(_from = nil, full: nil) def to_reference(_from = nil, full: nil)
id reference_link_text
end end
def reference_link_text(from = nil) def reference_link_text(from = nil)
......
...@@ -33,8 +33,9 @@ class Key < ActiveRecord::Base ...@@ -33,8 +33,9 @@ class Key < ActiveRecord::Base
after_destroy :refresh_user_cache after_destroy :refresh_user_cache
def key=(value) def key=(value)
write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) value&.delete!("\n\r")
value.strip! unless value.blank?
write_attribute(:key, value)
@public_key = nil @public_key = nil
end end
...@@ -96,7 +97,7 @@ class Key < ActiveRecord::Base ...@@ -96,7 +97,7 @@ class Key < ActiveRecord::Base
def generate_fingerprint def generate_fingerprint
self.fingerprint = nil self.fingerprint = nil
return unless public_key.valid? return unless self.key.present?
self.fingerprint = public_key.fingerprint self.fingerprint = public_key.fingerprint
end end
......
...@@ -314,7 +314,7 @@ class Member < ActiveRecord::Base ...@@ -314,7 +314,7 @@ class Member < ActiveRecord::Base
end end
def notification_setting def notification_setting
@notification_setting ||= user.notification_settings_for(source) @notification_setting ||= user&.notification_settings_for(source)
end end
def notifiable?(type, opts = {}) def notifiable?(type, opts = {})
......
...@@ -61,12 +61,9 @@ module Network ...@@ -61,12 +61,9 @@ module Network
@reserved[i] = [] @reserved[i] = []
end end
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436
Gitlab::GitalyClient.allow_n_plus_1_calls do
commits_sort_by_ref.each do |commit| commits_sort_by_ref.each do |commit|
place_chain(commit) place_chain(commit)
end end
end
# find parent spaces for not overlap lines # find parent spaces for not overlap lines
@commits.each do |c| @commits.each do |c|
......
...@@ -261,7 +261,7 @@ class Project < ActiveRecord::Base ...@@ -261,7 +261,7 @@ class Project < ActiveRecord::Base
validates :repository_storage, validates :repository_storage,
presence: true, presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: true validates :variables, variable_duplicates: { scope: :environment_scope }
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -10,6 +10,8 @@ class JiraService < IssueTrackerService ...@@ -10,6 +10,8 @@ class JiraService < IssueTrackerService
before_update :reset_password before_update :reset_password
alias_method :project_url, :url
# This is confusing, but JiraService does not really support these events. # This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service # The values here are required to display correct options in the service
# configuration screen. # configuration screen.
......
...@@ -6,10 +6,7 @@ class DeleteMergedBranchesService < BaseService ...@@ -6,10 +6,7 @@ class DeleteMergedBranchesService < BaseService
def execute def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438 branches = project.repository.merged_branch_names
Gitlab::GitalyClient.allow_n_plus_1_calls do
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests # Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names branches -= merge_request_branch_names
# Prevent deletion of protected branches # Prevent deletion of protected branches
...@@ -19,7 +16,6 @@ class DeleteMergedBranchesService < BaseService ...@@ -19,7 +16,6 @@ class DeleteMergedBranchesService < BaseService
DeleteBranchService.new(project, current_user).execute(branch) DeleteBranchService.new(project, current_user).execute(branch)
end end
end end
end
private private
......
module Issues
class FetchReferencedMergeRequestsService < Issues::BaseService
def execute(issue)
referenced_merge_requests = issue.referenced_merge_requests(current_user)
referenced_merge_requests = Gitlab::IssuableSorter.sort(project, referenced_merge_requests) { |i| i.iid.to_s }
closed_by_merge_requests = issue.closed_by_merge_requests(current_user)
closed_by_merge_requests = Gitlab::IssuableSorter.sort(project, closed_by_merge_requests) { |i| i.iid.to_s }
[referenced_merge_requests, closed_by_merge_requests]
end
end
end
...@@ -11,6 +11,7 @@ module Members ...@@ -11,6 +11,7 @@ module Members
Member.transaction do Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite? unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
member.destroy member.destroy
end end
......
...@@ -134,7 +134,7 @@ module MergeRequests ...@@ -134,7 +134,7 @@ module MergeRequests
end end
def append_closes_description def append_closes_description
return unless issue return unless issue&.to_reference.present?
closes_issue = "Closes #{issue.to_reference}" closes_issue = "Closes #{issue.to_reference}"
...@@ -160,10 +160,12 @@ module MergeRequests ...@@ -160,10 +160,12 @@ module MergeRequests
merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue) merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue)
unless merge_request.title return if merge_request.title.present?
if issue_iid.present?
merge_request.title = "Resolve #{issue.to_reference}"
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
merge_request.title = "Resolve #{issue_iid}" merge_request.title += " \"#{branch_title}\"" if branch_title.present?
merge_request.title += " \"#{branch_title}\"" unless branch_title.empty?
end end
end end
......
...@@ -5,11 +5,15 @@ module Projects ...@@ -5,11 +5,15 @@ module Projects
end end
def execute def execute
params[:file] = Gitlab::ProjectTemplate.find(params[:template_name]).file template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
params[:file] = file
GitlabProjectsImportService.new(current_user, params).execute
GitlabProjectsImportService.new(@current_user, @params).execute
ensure ensure
params[:file]&.close file&.close
end end
end end
end end
...@@ -11,12 +11,14 @@ module Projects ...@@ -11,12 +11,14 @@ module Projects
def execute def execute
FileUtils.mkdir_p(File.dirname(import_upload_path)) FileUtils.mkdir_p(File.dirname(import_upload_path))
file = params.delete(:file)
FileUtils.copy_entry(file.path, import_upload_path) FileUtils.copy_entry(file.path, import_upload_path)
Gitlab::ImportExport::ProjectCreator.new(params[:namespace_id], params[:import_type] = 'gitlab_project'
current_user, params[:import_source] = import_upload_path
import_upload_path,
params[:path]).execute ::Projects::CreateService.new(current_user, params).execute
end end
private private
...@@ -28,9 +30,5 @@ module Projects ...@@ -28,9 +30,5 @@ module Projects
def tmp_filename def tmp_filename
SecureRandom.hex SecureRandom.hex
end end
def file
params[:file]
end
end end
end end
# VariableDuplicatesValidator # VariableDuplicatesValidator
# #
# This validtor is designed for especially the following condition # This validator is designed for especially the following condition
# - Use `accepts_nested_attributes_for :xxx` in a parent model # - Use `accepts_nested_attributes_for :xxx` in a parent model
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model # - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first) if options[:scope]
scoped = value.group_by do |variable|
Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend
end
scoped.each_value { |scope| validate_duplicates(record, attribute, scope) }
else
validate_duplicates(record, attribute, value)
end
end
private
def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any? if duplicates.any?
record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}") error_message = "have duplicate values (#{duplicates.join(", ")})"
error_message += " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message)
end end
end end
end end
%li.flex-row %li.flex-row
.user-avatar .user-avatar
= image_tag avatar_icon(user), class: "avatar", alt: '' = image_tag avatar_icon_for_user(user), class: "avatar", alt: ''
.row-main-content .row-main-content
.user-name.row-title.str-truncated-100 .user-name.row-title.str-truncated-100
= link_to user.name, [:admin, user] = link_to user.name, [:admin, user]
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
= @user.name = @user.name
%ul.well-list %ul.well-list
%li %li
= image_tag avatar_icon(@user, 60), class: "avatar s60" = image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li %li
%span.light Profile page: %span.light Profile page:
%strong %strong
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.timeline-entry-inner .timeline-entry-inner
.timeline-icon .timeline-icon
= link_to user_path(discussion.author) do = link_to user_path(discussion.author) do
= image_tag avatar_icon(discussion.author), class: "avatar s40" = image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
.timeline-content .timeline-content
.discussion.js-toggle-container{ data: { discussion_id: discussion.id } } .discussion.js-toggle-container{ data: { discussion_id: discussion.id } }
.discussion-header .discussion-header
......
...@@ -10,7 +10,7 @@ xml.entry do ...@@ -10,7 +10,7 @@ xml.entry do
# eager-loaded. This allows us to re-use the user object's Email address, # eager-loaded. This allows us to re-use the user object's Email address,
# instead of having to run additional queries to figure out what Email to use # instead of having to run additional queries to figure out what Email to use
# for the avatar. # for the avatar.
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author)) xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_user(event.author))
xml.author do xml.author do
xml.username event.author_username xml.username event.author_username
......
- breadcrumb_title "Labels" - breadcrumb_title "Labels"
- page_title 'New Label' - page_title 'New Label'
- header_title group_title(@group, 'Labels', group_labels_path(@group))
%h3.page-title %h3.page-title
New Label New Label
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
.example .example
.cover-block .cover-block
.avatar-holder .avatar-holder
= image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: '' = image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title .cover-title
John Smith John Smith
......
...@@ -3,7 +3,7 @@ xml.entry do ...@@ -3,7 +3,7 @@ xml.entry do
xml.link href: project_issue_url(issue.project, issue) xml.link href: project_issue_url(issue.project, issue)
xml.title truncate(issue.title, length: 80) xml.title truncate(issue.title, length: 80)
xml.updated issue.updated_at.xmlschema xml.updated issue.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_user(issue.author))
xml.author do xml.author do
xml.name issue.author_name xml.name issue.author_name
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
......
...@@ -127,6 +127,19 @@ ...@@ -127,6 +127,19 @@
= link_to project_milestones_path(@project), title: 'Milestones' do = link_to project_milestones_path(@project), title: 'Milestones' do
%span %span
Milestones Milestones
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
= link_to issue_tracker.issue_tracker_path, class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('issue-external')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path do
%strong.fly-out-top-item-name
= issue_tracker.title
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %img.avatar{ height: "24", src: avatar_icon_for(commit.author, commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author - if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %img.avatar{ height: "24", src: avatar_icon_for(commit.committer, commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer - if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
triggered by triggered by
- if @pipeline.user - if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name = @pipeline.user.name
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %img.avatar{ height: "24", src: avatar_icon_for(commit.author, commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author - if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %img.avatar{ height: "24", src: avatar_icon_for(commit.committer, commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer - if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
triggered by triggered by
- if @pipeline.user - if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name = @pipeline.user.name
......
...@@ -20,8 +20,8 @@ ...@@ -20,8 +20,8 @@
or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
.col-lg-8 .col-lg-8
.clearfix.avatar-image.append-bottom-default .clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' = image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0= _("Upload new avatar") %h5.prepend-top-0= _("Upload new avatar")
.prepend-top-5.append-bottom-10 .prepend-top-5.append-bottom-10
%button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...") %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...")
......
...@@ -3,7 +3,7 @@ xml.entry do ...@@ -3,7 +3,7 @@ xml.entry do
xml.link href: project_commit_url(@project, id: commit.id) xml.link href: project_commit_url(@project, id: commit.id)
xml.title truncate(commit.title, length: 80, escape: false) xml.title truncate(commit.title, length: 80, escape: false)
xml.updated commit.committed_date.xmlschema xml.updated commit.committed_date.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_email(commit.author_email))
xml.author do |author| xml.author do |author|
xml.name commit.author_name xml.name commit.author_name
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
- if commit.status(ref) - if commit.status(ref)
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
#commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } } .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
= link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link" = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
......
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
markdown_docs_path: help_page_path('user/markdown'), markdown_docs_path: help_page_path('user/markdown'),
quick_actions_docs_path: help_page_path('user/project/quick_actions'), quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url, notes_path: notes_url,
close_issue_path: issue_path(@issue, issue: { state_event: :close }, format: 'json'),
reopen_issue_path: issue_path(@issue, issue: { state_event: :reopen }, format: 'json'),
last_fetched_at: Time.now.to_i, last_fetched_at: Time.now.to_i,
noteable_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } } current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
...@@ -21,30 +21,33 @@ ...@@ -21,30 +21,33 @@
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } } %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down') = icon('caret-down')
.droplab-dropdown
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } } %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request - if can_create_merge_request
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } } %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
.menu-item.droplab-item-ignore-hiding .menu-item
.icon-container.droplab-item-ignore-hiding= icon('check') = icon('check', class: 'icon')
.description.droplab-item-ignore-hiding Create merge request and branch = _('Create merge request and branch')
%li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } } %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
.menu-item.droplab-item-ignore-hiding .menu-item
.icon-container.droplab-item-ignore-hiding= icon('check') = icon('check', class: 'icon')
.description.droplab-item-ignore-hiding Create branch = _('Create branch')
%li.divider %li.divider.droplab-item-ignore
%li.droplab-item-ignore %li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
Branch name .form-group
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" } %label{ for: 'new-branch-name' }
%span.js-branch-message.branch-message.droplab-item-ignore = _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%li.droplab-item-ignore %span.js-branch-message.help-block
Source (branch or tag)
%input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } } .form-group
%span.js-ref-message.ref-message.droplab-item-ignore %label{ for: 'source-name' }
= _('Source (branch or tag)')
%li.droplab-item-ignore %input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } } %span.js-ref-message.help-block
Create merge request
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
= _('Create merge request')
by by
%a{ href: user_path(@build.user) } %a{ href: user_path(@build.user) }
%span.hidden-xs %span.hidden-xs
= image_tag avatar_icon(@build.user, 24), class: "avatar s24" = image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24"
%strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } } %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
= @build.user.name = @build.user.name
%strong.visible-xs-inline= @build.user.to_reference %strong.visible-xs-inline= @build.user.to_reference
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
Edit Edit
- if @project.group - if @project.group
= link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "You are about to promote #{@milestone.title} to a group level. This will make this milestone available to all projects inside #{@project.group.name}. The existing project milestone will be merged into the group level. This action cannot be reversed.", toggle: "tooltip" }, method: :post do = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting #{@milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do
Promote Promote
- if @milestone.active? - if @milestone.active?
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
author: { author: {
name: c.author_name, name: c.author_name,
email: c.author_email, email: c.author_email,
icon: image_path(avatar_icon(c.author_email, 20)) icon: image_path(avatar_icon_for_email(c.author_email, 20))
}, },
time: c.time, time: c.time,
space: c.spaces.first, space: c.spaces.first,
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= s_("PipelineSchedules|Inactive") = s_("PipelineSchedules|Inactive")
%td %td
- if pipeline_schedule.owner - if pipeline_schedule.owner
= image_tag avatar_icon(pipeline_schedule.owner, 20), class: "avatar s20" = image_tag avatar_icon_for_user(pipeline_schedule.owner, 20), class: "avatar s20"
= link_to user_path(pipeline_schedule.owner) do = link_to user_path(pipeline_schedule.owner) do
= pipeline_schedule.owner&.name = pipeline_schedule.owner&.name
%td %td
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%div %div
= link_to project_commits_path(@project, commit.id) do = link_to project_commits_path(@project, commit.id) do
%code= commit.short_id %code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' = image_tag avatar_icon_for_email(commit.author_email), class: "", width: 16, alt: ''
= markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author) = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td %td
%span.pull-right.cgray %span.pull-right.cgray
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= snippet.title = snippet.title
by by
= link_to user_snippets_path(snippet.author) do = link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author), class: "avatar avatar-inline s16", alt: '' = image_tag avatar_icon_for_user(snippet.author), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name = snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at) %span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title %h4.snippet-title
......
...@@ -18,6 +18,6 @@ ...@@ -18,6 +18,6 @@
%span %span
by by
= link_to user_snippets_path(snippet_title.author) do = link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author), class: "avatar avatar-inline s16", alt: '' = image_tag avatar_icon_for_user(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name = snippet_title.author_name
%span.light= time_ago_with_tooltip(snippet_title.created_at) %span.light= time_ago_with_tooltip(snippet_title.created_at)
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
.pull-right.hidden-xs.hidden-sm .pull-right.hidden-xs.hidden-sm
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting #{label.title} will make it available for all projects inside #{label.project.group.name}. Existing project labels with the same name will be merged. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
%span.sr-only Promote to Group %span.sr-only Promote to Group
= sprite_icon('level-up') = sprite_icon('level-up')
- if can?(current_user, :admin_label, label) - if can?(current_user, :admin_label, label)
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%li.member{ class: dom_class(member), id: dom_id(member) } %li.member{ class: dom_class(member), id: dom_id(member) }
%span.list-item-name %span.list-item-name
- if user - if user
= image_tag avatar_icon(user, 40), class: "avatar s40", alt: '' = image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
.user-info .user-info
= link_to user.name, user_path(user), class: 'member' = link_to user.name, user_path(user), class: 'member'
%span.cgray= user.to_reference %span.cgray= user.to_reference
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
Expires in #{distance_of_time_in_words_to_now(member.expires_at)} Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else - else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: '' = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40", alt: ''
.user-info .user-info
.member= member.invite_email .member= member.invite_email
.cgray .cgray
......
...@@ -28,4 +28,4 @@ ...@@ -28,4 +28,4 @@
- assignees.each do |assignee| - assignees.each do |assignee|
= link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }), = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '') - image_tag(avatar_icon_for_user(assignee, 16), class: "avatar s16", alt: '')
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
\ \
- if @project.group - if @project.group
= link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "You are about to promote #{milestone.title} to a group level. This will make this milestone available to all projects inside #{@project.group.name}. The existing project milestone will be merged into the group level. This action cannot be reversed.", toggle: "tooltip" }, method: :post do = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting #{milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do
Promote Promote
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- users.each do |user| - users.each do |user|
%li %li
= link_to user, title: user.name, class: "darken" do = link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32" = image_tag avatar_icon_for_user(user, 32), class: "avatar s32"
%strong= truncate(user.name, length: 40) %strong= truncate(user.name, length: 40)
%div %div
%small.cgray= user.username %small.cgray= user.username
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= icon_for_system_note(note) = icon_for_system_note(note)
- else - else
%a.image-diff-avatar-link{ href: user_path(note.author) } %a.image-diff-avatar-link{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' = image_tag avatar_icon_for_user(note.author), alt: '', class: 'avatar s40'
- if note.is_a?(DiffNote) && note.on_image? - if note.is_a?(DiffNote) && note.on_image?
- if show_image_comment_badge && note_counter == 0 - if show_image_comment_badge && note_counter == 0
-# Only show this for the first comment in the discussion -# Only show this for the first comment in the discussion
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.timeline-icon.hidden-xs.hidden-sm .timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) } %a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' = image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form .timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user - elsif !current_user
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
.avatar-container.s40 .avatar-container.s40
= link_to project_path(project), class: dom_class(project) do = link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar - if project.creator && use_creator_avatar
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' = image_tag avatar_icon_for_user(project.creator, 40), class: "avatar s40", alt:''
- else - else
= project_icon(project, alt: '', class: 'avatar project-avatar s40') = project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details .project-details
......
- link_project = local_assigns.fetch(:link_project, false) - link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row %li.snippet-row
= image_tag avatar_icon(snippet.author), class: "avatar s40 hidden-xs", alt: '' = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 hidden-xs", alt: ''
.title .title
= link_to reliable_snippet_path(snippet) do = link_to reliable_snippet_path(snippet) do
......
...@@ -35,8 +35,8 @@ ...@@ -35,8 +35,8 @@
.profile-header .profile-header
.avatar-holder .avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' = image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
.user-info .user-info
.cover-title .cover-title
......
...@@ -7,6 +7,7 @@ class CheckGcpProjectBillingWorker ...@@ -7,6 +7,7 @@ class CheckGcpProjectBillingWorker
LEASE_TIMEOUT = 3.seconds.to_i LEASE_TIMEOUT = 3.seconds.to_i
SESSION_KEY_TIMEOUT = 5.minutes SESSION_KEY_TIMEOUT = 5.minutes
BILLING_TIMEOUT = 1.hour BILLING_TIMEOUT = 1.hour
BILLING_CHANGED_LABELS = { state_transition: nil }.freeze
def self.get_session_token(token_key) def self.get_session_token(token_key)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
...@@ -22,8 +23,11 @@ class CheckGcpProjectBillingWorker ...@@ -22,8 +23,11 @@ class CheckGcpProjectBillingWorker
end end
end end
def self.redis_shared_state_key_for(token) def self.get_billing_state(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled" Gitlab::Redis::SharedState.with do |redis|
value = redis.get(redis_shared_state_key_for(token))
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
end
end end
def perform(token_key) def perform(token_key)
...@@ -33,12 +37,9 @@ class CheckGcpProjectBillingWorker ...@@ -33,12 +37,9 @@ class CheckGcpProjectBillingWorker
return unless token return unless token
return unless try_obtain_lease_for(token) return unless try_obtain_lease_for(token)
billing_enabled_projects = CheckGcpProjectBillingService.new.execute(token) billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty?
Gitlab::Redis::SharedState.with do |redis| update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state)
redis.set(self.class.redis_shared_state_key_for(token), self.class.set_billing_state(token, billing_enabled_state)
!billing_enabled_projects.empty?,
ex: BILLING_TIMEOUT)
end
end end
private private
...@@ -51,9 +52,41 @@ class CheckGcpProjectBillingWorker ...@@ -51,9 +52,41 @@ class CheckGcpProjectBillingWorker
"gitlab:gcp:session:#{token_key}" "gitlab:gcp:session:#{token_key}"
end end
def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
end
def self.set_billing_state(token, value)
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT)
end
end
def try_obtain_lease_for(token) def try_obtain_lease_for(token)
Gitlab::ExclusiveLease Gitlab::ExclusiveLease
.new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT) .new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
.try_obtain .try_obtain
end end
def billing_changed_counter
@billing_changed_counter ||= Gitlab::Metrics.counter(
:gcp_billing_change_count,
"Counts the number of times a GCP project changed billing_enabled state from false to true",
BILLING_CHANGED_LABELS
)
end
def state_transition(previous_state, current_state)
if previous_state.nil? && !current_state
'no_billing'
elsif previous_state.nil? && current_state
'with_billing'
elsif !previous_state && current_state
'billing_configured'
end
end
def update_billing_change_counter(previous_state, current_state)
billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state))
end
end end
---
title: Group MRs on issue page by project and namespace.
merge_request: 8494
author: Jeff Stubler
---
title: Prevent MR Widget error when no CI configured
merge_request:
author:
type: fixed
---
title: Fix Teleporting Emoji
merge_request: 16963
author: Jared Deckard <jared.deckard@gmail.com>
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.
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.
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