Commit 58d1ad56 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into qa-clone-with-deploy-key

* upstream/master: (69 commits)
  Change issue show page to group MRs by projects and namespaces
  Merge branch 'master-i18n' into 'master'
  Update sidekiq_style_guide.md
  Document all_queues.yml in sidekiq_style_guide.md
  Fix artifact creation
  Fix Error 500s creating merge requests with external issue tracker
  Addressed mr observations
  Clean new Flash() and stop disabling no-new (eslint) when possible
  Disable query limiting warnings for now on GitLab.com
  Dry up spec
  Add changelog entry
  Schedule PopulateUntrackedUploads if needed
  Fix orphan temp table untracked_files_for_uploads
  Fix last batch size equals max batch size error
  Revert "Merge branch 'rd-40552-gitlab-should-check-if-keys-are-valid-before-saving' into 'master'"
  Fix warning messages for promoting labels and milestones
  Fixed missing js selector for the realtime pipelines commit comp
  Reuse getter Add loading button for better UX
  Honour workhorse provided file name
  Fix a transient failure in db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb where symlink already exists
  ...
parents ab4f8032 bf5e617a
...@@ -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);
......
...@@ -39,7 +39,7 @@ export default class VariableList { ...@@ -39,7 +39,7 @@ export default class VariableList {
}, },
protected: { protected: {
selector: '.js-ci-variable-input-protected', selector: '.js-ci-variable-input-protected',
default: 'true', default: 'false',
}, },
environment_scope: { environment_scope: {
// We can't use a `.js-` class here because // We can't use a `.js-` class here because
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
// Width where images must fits in, for 2-up this gets divided by 2 // Width where images must fits in, for 2-up this gets divided by 2
const availWidth = 900; const availWidth = 900;
......
...@@ -6,5 +6,5 @@ import 'vendor/jquery.endless-scroll'; ...@@ -6,5 +6,5 @@ import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret'; import 'vendor/jquery.caret';
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo'; import 'vendor/jquery.scrollTo';
import 'vendor/jquery.waitforimages'; import 'jquery.waitforimages';
import 'select2/select2'; import 'select2/select2';
...@@ -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: {
......
<script> <script>
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import { humanize } from '../../lib/utils/text_utility'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
...@@ -21,14 +22,18 @@ ...@@ -21,14 +22,18 @@
export default { export default {
components: { components: {
userAvatarLink, UserAvatarLink,
'commit-component': CommitComponent, CommitComponent,
'actions-component': ActionsComponent, ActionsComponent,
'external-url-component': ExternalUrlComponent, ExternalUrlComponent,
'stop-component': StopComponent, StopComponent,
'rollback-component': RollbackComponent, RollbackComponent,
'terminal-button-component': TerminalButtonComponent, TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent, MonitoringButtonComponent,
},
directives: {
tooltip,
}, },
props: { props: {
...@@ -443,7 +448,11 @@ ...@@ -443,7 +448,11 @@
v-if="!model.isFolder" v-if="!model.isFolder"
class="environment-name flex-truncate-parent table-mobile-content" class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath"> :href="environmentPath">
<span class="flex-truncate-child">{{ model.name }}</span> <span
class="flex-truncate-child"
v-tooltip
:title="model.name"
>{{ model.name }}</span>
</a> </a>
<span <span
v-else v-else
......
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 {
......
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
class ImporterStatus { class ImporterStatus {
constructor(jobsUrl, importUrl) { constructor(jobsUrl, importUrl) {
this.jobsUrl = jobsUrl; this.jobsUrl = jobsUrl;
...@@ -9,7 +13,20 @@ class ImporterStatus { ...@@ -9,7 +13,20 @@ class ImporterStatus {
initStatusPage() { initStatusPage() {
$('.js-add-to-import') $('.js-add-to-import')
.off('click') .off('click')
.on('click', (event) => { .on('click', this.addToImport.bind(this));
$('.js-import-all')
.off('click')
.on('click', function onClickImportAll() {
const $btn = $(this);
$btn.disable().addClass('is-loading');
return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click');
});
});
}
addToImport(event) {
const $btn = $(event.currentTarget); const $btn = $(event.currentTarget);
const $tr = $btn.closest('tr'); const $tr = $btn.closest('tr');
const $targetField = $tr.find('.import-target'); const $targetField = $tr.find('.import-target');
...@@ -24,24 +41,22 @@ class ImporterStatus { ...@@ -24,24 +41,22 @@ class ImporterStatus {
} }
$btn.disable().addClass('is-loading'); $btn.disable().addClass('is-loading');
return $.post(this.importUrl, { return axios.post(this.importUrl, {
repo_id: id, repo_id: id,
target_namespace: targetNamespace, target_namespace: targetNamespace,
new_name: newName, new_name: newName,
}, { })
dataType: 'script', .then(({ data }) => {
}); const job = $(`tr#repo_${id}`);
}); job.attr('id', `project_${data.id}`);
$('.js-import-all') job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
.off('click') $('table.import-jobs tbody').prepend(job);
.on('click', function onClickImportAll() {
const $btn = $(this); job.addClass('active');
$btn.disable().addClass('is-loading'); job.find('.import-actions').html('<i class="fa fa-spinner fa-spin" aria-label="importing"></i> started');
return $('.js-add-to-import').each(function triggerAddImport() { })
return $(this).trigger('click'); .catch(() => flash(__('An error occurred while importing project')));
});
});
} }
setAutoUpdate() { setAutoUpdate() {
...@@ -71,7 +86,7 @@ class ImporterStatus { ...@@ -71,7 +86,7 @@ class ImporterStatus {
} }
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
export default function initImporterStatus() { function initImporterStatus() {
const importerStatus = document.querySelector('.js-importer-status'); const importerStatus = document.querySelector('.js-importer-status');
if (importerStatus) { if (importerStatus) {
...@@ -79,3 +94,8 @@ export default function initImporterStatus() { ...@@ -79,3 +94,8 @@ export default function initImporterStatus() {
return new ImporterStatus(data.jobsImportPath, data.importPath); return new ImporterStatus(data.jobsImportPath, data.importPath);
} }
} }
export {
initImporterStatus as default,
ImporterStatus,
};
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility'; import { addDelimiter } from './lib/utils/text_utility';
import flash from './flash'; import flash from './flash';
...@@ -25,32 +24,29 @@ export default class Issue { ...@@ -25,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);
...@@ -73,6 +69,28 @@ export default class Issue { ...@@ -73,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(() => {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
import 'vendor/jquery.waitforimages';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TaskList from './task_list'; import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs'; import MergeRequestTabs from './merge_request_tabs';
......
...@@ -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();
......
/* 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')));
}
} }
...@@ -30,6 +30,9 @@ export default function renderMermaid($els) { ...@@ -30,6 +30,9 @@ export default function renderMermaid($els) {
$els.each((i, el) => { $els.each((i, el) => {
const source = el.textContent; const source = el.textContent;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
mermaid.init(undefined, el, (id) => { mermaid.init(undefined, el, (id) => {
const svg = document.getElementById(id); const svg = document.getElementById(id);
......
...@@ -2,7 +2,7 @@ import _ from 'underscore'; ...@@ -2,7 +2,7 @@ import _ from 'underscore';
import '~/smart_interval'; import '~/smart_interval';
import timeTracker from './time_tracker'; import IssuableTimeTracker from './time_tracker.vue';
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator'; import Mediator from '../../sidebar_mediator';
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
}; };
}, },
components: { components: {
'issuable-time-tracker': timeTracker, IssuableTimeTracker,
}, },
methods: { methods: {
listenForQuickActions() { listenForQuickActions() {
......
<script>
import timeTrackingHelpState from './help_state'; import timeTrackingHelpState from './help_state';
import timeTrackingCollapsedState from './collapsed_state'; import timeTrackingCollapsedState from './collapsed_state';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import timeTrackingSpentOnlyPane from './spent_only_pane';
...@@ -8,7 +9,15 @@ import timeTrackingComparisonPane from './comparison_pane'; ...@@ -8,7 +9,15 @@ import timeTrackingComparisonPane from './comparison_pane';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
name: 'issuable-time-tracker', name: 'IssuableTimeTracker',
components: {
'time-tracking-collapsed-state': timeTrackingCollapsedState,
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
'time-tracking-comparison-pane': timeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
},
props: { props: {
time_estimate: { time_estimate: {
type: Number, type: Number,
...@@ -38,14 +47,6 @@ export default { ...@@ -38,14 +47,6 @@ export default {
showHelp: false, showHelp: false,
}; };
}, },
components: {
'time-tracking-collapsed-state': timeTrackingCollapsedState,
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
'time-tracking-comparison-pane': timeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
},
computed: { computed: {
timeSpent() { timeSpent() {
return this.time_spent; return this.time_spent;
...@@ -81,6 +82,9 @@ export default { ...@@ -81,6 +82,9 @@ export default {
return !!this.showHelp; return !!this.showHelp;
}, },
}, },
created() {
eventHub.$on('timeTracker:updateData', this.update);
},
methods: { methods: {
toggleHelpState(show) { toggleHelpState(show) {
this.showHelp = show; this.showHelp = show;
...@@ -92,10 +96,10 @@ export default { ...@@ -92,10 +96,10 @@ export default {
this.human_time_spent = data.human_time_spent; this.human_time_spent = data.human_time_spent;
}, },
}, },
created() { };
eventHub.$on('timeTracker:updateData', this.update); </script>
},
template: ` <template>
<div <div
class="time_tracker time-tracking-component-wrap" class="time_tracker time-tracking-component-wrap"
v-cloak v-cloak
...@@ -119,7 +123,8 @@ export default { ...@@ -119,7 +123,8 @@ export default {
<i <i
class="fa fa-question-circle" class="fa fa-question-circle"
aria-hidden="true" aria-hidden="true"
/> >
</i>
</div> </div>
<div <div
class="close-help-button pull-right" class="close-help-button pull-right"
...@@ -129,7 +134,8 @@ export default { ...@@ -129,7 +134,8 @@ export default {
<i <i
class="fa fa-close" class="fa fa-close"
aria-hidden="true" aria-hidden="true"
/> >
</i>
</div> </div>
</div> </div>
<div class="time-tracking-content hide-collapsed"> <div class="time-tracking-content hide-collapsed">
...@@ -154,10 +160,9 @@ export default { ...@@ -154,10 +160,9 @@ export default {
<transition name="help-state-toggle"> <transition name="help-state-toggle">
<time-tracking-help-state <time-tracking-help-state
v-if="showHelpState" v-if="showHelpState"
:rootPath="rootPath" :root-path="rootPath"
/> />
</transition> </transition>
</div> </div>
</div> </div>
`, </template>
};
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
statusIcon,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
message() {
return `If the ${this.missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line`;
},
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
{{missingBranchName}}
</span> branch does not exist.
Please restore it or use a different {{missingBranchName}} branch
<i
v-tooltip
class="fa fa-question-circle"
:title="message"
:aria-label="message"></i>
</span>
</div>
</div>
`,
};
<script>
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
directives: {
tooltip,
},
components: {
mrWidgetMergeHelp,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
missingBranchNameMessage() {
return sprintf(s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), {
missingBranchName: this.missingBranchName,
});
},
message() {
return sprintf(s__('mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line'), {
missingBranchName: this.missingBranchName,
});
},
},
};
</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 js-branch-text">
<span class="capitalize">
{{ missingBranchName }}
</span> {{ s__("mrWidget|branch does not exist.") }}
{{ missingBranchNameMessage }}
<i
v-tooltip
class="fa fa-question-circle"
:title="message"
:aria-label="message"
>
</i>
</span>
</div>
</div>
</template>
...@@ -24,7 +24,7 @@ export { default as WipState } from './components/states/mr_widget_wip'; ...@@ -24,7 +24,7 @@ export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
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'; 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';
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';
......
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
export default {
props: {
inputId: {
type: String,
required: true,
},
confirmationKey: {
type: String,
required: true,
},
confirmationValue: {
type: String,
required: true,
},
shouldEscapeConfirmationValue: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
inputLabel() {
let value = this.confirmationValue;
if (this.shouldEscapeConfirmationValue) {
value = _.escape(value);
}
return sprintf(
__('Type %{value} to confirm:'),
{ value: `<code>${value}</code>` },
false,
);
},
},
methods: {
hasCorrectValue() {
return this.$refs.enteredValue.value === this.confirmationValue;
},
},
};
</script>
<template>
<div>
<label
v-html="inputLabel"
:for="inputId"
>
</label>
<input
:id="inputId"
:name="confirmationKey"
type="text"
ref="enteredValue"
class="form-control"
/>
</div>
</template>
...@@ -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',
}, },
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
@import "framework/flash"; @import "framework/flash";
@import "framework/forms"; @import "framework/forms";
@import "framework/gfm"; @import "framework/gfm";
@import "framework/gitlab-theme"; @import "framework/gitlab_theme";
@import "framework/header"; @import "framework/header";
@import "framework/highlight"; @import "framework/highlight";
@import "framework/issue_box"; @import "framework/issue_box";
...@@ -35,10 +35,10 @@ ...@@ -35,10 +35,10 @@
@import "framework/pagination"; @import "framework/pagination";
@import "framework/panels"; @import "framework/panels";
@import "framework/popup"; @import "framework/popup";
@import "framework/secondary-navigation-elements"; @import "framework/secondary_navigation_elements";
@import "framework/selects"; @import "framework/selects";
@import "framework/sidebar"; @import "framework/sidebar";
@import "framework/contextual-sidebar"; @import "framework/contextual_sidebar";
@import "framework/tables"; @import "framework/tables";
@import "framework/notes"; @import "framework/notes";
@import "framework/tabs"; @import "framework/tabs";
...@@ -49,16 +49,16 @@ ...@@ -49,16 +49,16 @@
@import "framework/zen"; @import "framework/zen";
@import "framework/blank"; @import "framework/blank";
@import "framework/wells"; @import "framework/wells";
@import "framework/page-header"; @import "framework/page_header";
@import "framework/awards"; @import "framework/awards";
@import "framework/images"; @import "framework/images";
@import "framework/broadcast-messages"; @import "framework/broadcast_messages";
@import "framework/emojis"; @import "framework/emojis";
@import "framework/emoji-sprites"; @import "framework/emoji_sprites";
@import "framework/icons"; @import "framework/icons";
@import "framework/snippets"; @import "framework/snippets";
@import "framework/memory_graph"; @import "framework/memory_graph";
@import "framework/responsive_tables"; @import "framework/responsive_tables";
@import "framework/stacked-progress-bar"; @import "framework/stacked_progress_bar";
@import "framework/ci_variable_list"; @import "framework/ci_variable_list";
@import "framework/feature_highlight"; @import "framework/feature_highlight";
...@@ -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 {
......
...@@ -121,6 +121,10 @@ ...@@ -121,6 +121,10 @@
width: 100%; width: 100%;
text-align: left; text-align: left;
} }
.environment-child-row {
padding-left: 20px;
}
} }
} }
......
...@@ -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 {
......
...@@ -2,26 +2,16 @@ class Import::BaseController < ApplicationController ...@@ -2,26 +2,16 @@ class Import::BaseController < ApplicationController
private private
def find_or_create_namespace(names, owner) def find_or_create_namespace(names, owner)
return current_user.namespace if names == owner
return current_user.namespace unless current_user.can_create_group?
names = params[:target_namespace].presence || names names = params[:target_namespace].presence || names
full_path_namespace = Namespace.find_by_full_path(names)
return full_path_namespace if full_path_namespace return current_user.namespace if names == owner
group = Groups::NestedCreateService.new(current_user, group_path: names).execute
names.split('/').inject(nil) do |parent, name| group.errors.any? ? current_user.namespace : group
begin rescue => e
namespace = Group.create!(name: name, Gitlab::AppLogger.error(e)
path: name,
owner: current_user,
parent: parent)
namespace.add_owner(current_user)
namespace current_user.namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.where(parent: parent).find_by_path_or_name(name)
end
end
end end
end end
...@@ -37,24 +37,30 @@ class Import::BitbucketController < Import::BaseController ...@@ -37,24 +37,30 @@ class Import::BitbucketController < Import::BaseController
def create def create
bitbucket_client = Bitbucket::Client.new(credentials) bitbucket_client = Bitbucket::Client.new(credentials)
@repo_id = params[:repo_id].to_s repo_id = params[:repo_id].to_s
name = @repo_id.gsub('___', '/') name = repo_id.gsub('___', '/')
repo = bitbucket_client.repo(name) repo = bitbucket_client.repo(name)
@project_name = params[:new_name].presence || repo.name project_name = params[:new_name].presence || repo.name
repo_owner = repo.owner repo_owner = repo.owner
repo_owner = current_user.username if repo_owner == bitbucket_client.user.username repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
namespace_path = params[:new_namespace].presence || repo_owner namespace_path = params[:new_namespace].presence || repo_owner
target_namespace = find_or_create_namespace(namespace_path, current_user)
@target_namespace = find_or_create_namespace(namespace_path, current_user) if current_user.can?(:create_projects, target_namespace)
if current_user.can?(:create_projects, @target_namespace)
# The token in a session can be expired, we need to get most recent one because # The token in a session can be expired, we need to get most recent one because
# Bitbucket::Connection class refreshes it. # Bitbucket::Connection class refreshes it.
session[:bitbucket_token] = bitbucket_client.connection.token session[:bitbucket_token] = bitbucket_client.connection.token
@project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, credentials).execute
project = Gitlab::BitbucketImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, credentials).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
end
else else
render 'unauthorized' render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
end end
end end
......
...@@ -58,17 +58,17 @@ class Import::FogbugzController < Import::BaseController ...@@ -58,17 +58,17 @@ class Import::FogbugzController < Import::BaseController
end end
def create def create
@repo_id = params[:repo_id] repo = client.repo(params[:repo_id])
repo = client.repo(@repo_id)
fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] } fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
@target_namespace = current_user.namespace
@project_name = repo.name
namespace = @target_namespace
umap = session[:fogbugz_user_map] || client.user_map umap = session[:fogbugz_user_map] || client.user_map
@project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, namespace, current_user, umap).execute project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, current_user.namespace, current_user, umap).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
end
end end
private private
......
...@@ -36,16 +36,21 @@ class Import::GithubController < Import::BaseController ...@@ -36,16 +36,21 @@ class Import::GithubController < Import::BaseController
end end
def create def create
@repo_id = params[:repo_id].to_i repo = client.repo(params[:repo_id].to_i)
repo = client.repo(@repo_id) project_name = params[:new_name].presence || repo.name
@project_name = params[:new_name].presence || repo.name
namespace_path = params[:target_namespace].presence || current_user.namespace_path namespace_path = params[:target_namespace].presence || current_user.namespace_path
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if can?(current_user, :create_projects, @target_namespace) if can?(current_user, :create_projects, target_namespace)
@project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, access_params, type: provider).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
end
else else
render 'unauthorized' render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
end end
end end
......
...@@ -24,15 +24,19 @@ class Import::GitlabController < Import::BaseController ...@@ -24,15 +24,19 @@ class Import::GitlabController < Import::BaseController
end end
def create def create
@repo_id = params[:repo_id].to_i repo = client.project(params[:repo_id].to_i)
repo = client.project(@repo_id) target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username'])
@project_name = repo['name']
@target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username'])
if current_user.can?(:create_projects, @target_namespace) if current_user.can?(:create_projects, target_namespace)
@project = Gitlab::GitlabImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute project = Gitlab::GitlabImport::ProjectCreator.new(repo, target_namespace, current_user, access_params).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
end
else else
render 'unauthorized' render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
end end
end end
......
...@@ -85,16 +85,16 @@ class Import::GoogleCodeController < Import::BaseController ...@@ -85,16 +85,16 @@ class Import::GoogleCodeController < Import::BaseController
end end
def create def create
@repo_id = params[:repo_id] repo = client.repo(params[:repo_id])
repo = client.repo(@repo_id)
@target_namespace = current_user.namespace
@project_name = repo.name
namespace = @target_namespace
user_map = session[:google_code_user_map] user_map = session[:google_code_user_map]
@project = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, current_user, user_map).execute project = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, current_user.namespace, current_user, user_map).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
end
end end
private private
......
...@@ -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
......
# Snippets Finder
#
# Used to filter Snippets collections by a set of params
#
# Arguments.
#
# current_user - The current user, nil also can be used.
# params:
# visibility (integer) - Individual snippet visibility: Public(20), internal(10) or private(0).
# project (Project) - Project related.
# author (User) - Author related.
#
# params are optional
class SnippetsFinder < UnionFinder class SnippetsFinder < UnionFinder
attr_accessor :current_user, :params include Gitlab::Allowable
attr_accessor :current_user, :params, :project
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
@project = params[:project]
end end
def execute def execute
items = init_collection items = init_collection
items = by_project(items)
items = by_author(items) items = by_author(items)
items = by_visibility(items) items = by_visibility(items)
...@@ -18,25 +32,42 @@ class SnippetsFinder < UnionFinder ...@@ -18,25 +32,42 @@ class SnippetsFinder < UnionFinder
private private
def init_collection def init_collection
items = Snippet.all if project.present?
authorized_snippets_from_project
else
authorized_snippets
end
end
accessible(items) def authorized_snippets_from_project
if can?(current_user, :read_project_snippet, project)
if project.team.member?(current_user)
project.snippets
else
project.snippets.public_to_user(current_user)
end
else
Snippet.none
end
end end
def accessible(items) def authorized_snippets
segments = [] Snippet.where(feature_available_projects.or(not_project_related)).public_or_visible_to_user(current_user)
segments << items.public_to_user(current_user) end
segments << authorized_to_user(items) if current_user
def feature_available_projects
projects = Project.public_or_visible_to_user(current_user)
.with_feature_available_for_user(:snippets, current_user).select(:id)
arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql)
table[:project_id].in(arel_query)
end
find_union(segments, Snippet.includes(:author)) def not_project_related
table[:project_id].eq(nil)
end end
def authorized_to_user(items) def table
items.where( Snippet.arel_table
'author_id = :author_id
OR project_id IN (:project_ids)',
author_id: current_user.id,
project_ids: current_user.authorized_projects.select(:id))
end end
def by_visibility(items) def by_visibility(items)
...@@ -53,12 +84,6 @@ class SnippetsFinder < UnionFinder ...@@ -53,12 +84,6 @@ class SnippetsFinder < UnionFinder
items.where(author_id: params[:author].id) items.where(author_id: params[:author].id)
end end
def by_project(items)
return items unless params[:project]
items.where(project_id: params[:project].id)
end
def visibility_from_scope def visibility_from_scope
case params[:scope].to_s case params[:scope].to_s
when 'are_private' when 'are_private'
......
...@@ -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
......
...@@ -1589,9 +1589,12 @@ class Project < ActiveRecord::Base ...@@ -1589,9 +1589,12 @@ class Project < ActiveRecord::Base
end end
def protected_for?(ref) def protected_for?(ref)
ProtectedBranch.protected?(self, ref) || if repository.branch_exists?(ref)
ProtectedBranch.protected?(self, ref)
elsif repository.tag_exists?(ref)
ProtectedTag.protected?(self, ref) ProtectedTag.protected?(self, ref)
end end
end
def deployment_variables def deployment_variables
return [] unless deployment_platform return [] unless deployment_platform
......
...@@ -74,6 +74,27 @@ class Snippet < ActiveRecord::Base ...@@ -74,6 +74,27 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end end
# Returns a collection of snippets that are either public or visible to the
# logged in user.
#
# This method does not verify the user actually has the access to the project
# the snippet is in, so it should be only used on a relation that's already scoped
# for project access
def self.public_or_visible_to_user(user = nil)
if user
authorized = user
.project_authorizations
.select(1)
.where('project_authorizations.project_id = snippets.project_id')
levels = Gitlab::VisibilityLevel.levels_for_user(user)
where('EXISTS (?) OR snippets.visibility_level IN (?) or snippets.author_id = (?)', authorized, levels, user.id)
else
public_to_user
end
end
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}" reference = "#{self.class.reference_prefix}#{id}"
......
...@@ -119,7 +119,6 @@ class ProjectPolicy < BasePolicy ...@@ -119,7 +119,6 @@ class ProjectPolicy < BasePolicy
enable :create_note enable :create_note
enable :upload_file enable :upload_file
enable :read_cycle_analytics enable :read_cycle_analytics
enable :read_project_snippet
end end
rule { can?(:reporter_access) }.policy do rule { can?(:reporter_access) }.policy do
......
class ProjectSerializer < BaseSerializer
entity ProjectEntity
end
...@@ -11,8 +11,8 @@ module Groups ...@@ -11,8 +11,8 @@ module Groups
def execute def execute
return nil unless group_path return nil unless group_path
if group = Group.find_by_full_path(group_path) if namespace = namespace_or_group(group_path)
return group return namespace
end end
if group_path.include?('/') && !Group.supports_nested_groups? if group_path.include?('/') && !Group.supports_nested_groups?
...@@ -40,10 +40,14 @@ module Groups ...@@ -40,10 +40,14 @@ module Groups
) )
new_params[:visibility_level] ||= Gitlab::CurrentSettings.current_application_settings.default_group_visibility new_params[:visibility_level] ||= Gitlab::CurrentSettings.current_application_settings.default_group_visibility
last_group = Group.find_by_full_path(partial_path) || Groups::CreateService.new(current_user, new_params).execute last_group = namespace_or_group(partial_path) || Groups::CreateService.new(current_user, new_params).execute
end end
last_group last_group
end end
def namespace_or_group(group_path)
Namespace.find_by_full_path(group_path)
end
end end
end end
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
...@@ -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?
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
if issue_iid.present?
merge_request.title = "Resolve #{issue_iid}" merge_request.title = "Resolve #{issue_iid}"
merge_request.title += " \"#{branch_title}\"" unless branch_title.empty? branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
merge_request.title += " \"#{branch_title}\"" if branch_title.present?
end end
end end
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- id = variable&.id - id = variable&.id
- key = variable&.key - key = variable&.key
- value = variable&.value - value = variable&.value
- is_protected = variable && !only_key_value ? variable.protected : true - is_protected = variable && !only_key_value ? variable.protected : false
- id_input_name = "#{form_field}[variables_attributes][][id]" - id_input_name = "#{form_field}[variables_attributes][][id]"
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]" - destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
......
- 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
......
- if @project.persisted?
:plain
job = $("tr#repo_#{@repo_id}")
job.attr("id", "project_#{@project.id}")
target_field = job.find(".import-target")
target_field.empty()
target_field.append('#{link_to @project.full_path, project_path(@project)}')
$("table.import-jobs tbody").prepend(job)
job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started")
- else
:plain
job = $("tr#repo_#{@repo_id}")
job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(h(@project.errors.full_messages.join(',')))}")
:plain
tr = $("tr#repo_#{@repo_id}")
target_field = tr.find(".import-target")
import_button = tr.find(".btn-import")
origin_target = target_field.text()
project_name = "#{@project_name}"
origin_namespace = "#{@target_namespace.full_path}"
target_field.empty()
target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>")
target_field.append("<input type='text' name='target_namespace' />")
target_field.append("/" + project_name)
target_field.data("project_name", project_name)
target_field.find('input').prop("value", origin_namespace)
import_button.enable().removeClass('is-loading')
...@@ -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')
...@@ -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?
......
...@@ -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)
......
...@@ -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"
......
---
title: Group MRs on issue page by project and namespace.
merge_request: 8494
author: Jeff Stubler
---
title: Sanitize extra blank spaces used when uploading a SSH key
merge_request: 40552
author:
type: fixed
---
title: Adds tooltip in environment names to increase readability
merge_request:
author:
type: fixed
---
title: Fix close button on issues not working on mobile
merge_request:
author:
type: fixed
---
title: Create empty wiki when import from GitLab and wiki is not there
merge_request:
author:
type: fixed
---
title: Update vue component naming guidelines
merge_request: 17018
author: George Tsiolis
type: other
---
title: Fix breadcrumb on labels page for groups
merge_request: 17045
author: Onuwa Nnachi Isaac
type: fixed
---
title: Updated the katex library
merge_request: 15864
author:
type: other
---
title: Resolve PrepareUntrackedUploads PostgreSQL syntax error
merge_request: 17019
author:
type: fixed
---
title: Move IssuableTimeTracker vue component
merge_request: 16948
author: George Tsiolis
type: performance
---
title: Cleanup new branch/merge request form in issues
merge_request: 16854
author:
type: fixed
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/redis/queues') require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state') require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
require_dependency Rails.root.join('lib/gitlab/request_context') require_dependency Rails.root.join('lib/gitlab/request_context')
require_dependency Rails.root.join('lib/gitlab/current_settings')
# Settings in config/environments/* take precedence over those specified here. # Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers # Application configuration should go into files in config/initializers
...@@ -107,8 +108,6 @@ module Gitlab ...@@ -107,8 +108,6 @@ module Gitlab
config.assets.precompile << "print.css" config.assets.precompile << "print.css"
config.assets.precompile << "notify.css" config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css" config.assets.precompile << "mailers/*.css"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "performance_bar.css" config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js" config.assets.precompile << "lib/ace.js"
......
...@@ -153,6 +153,27 @@ var config = { ...@@ -153,6 +153,27 @@ var config = {
name: '[name].[hash].[ext]', name: '[name].[hash].[ext]',
} }
}, },
{
test: /katex.css$/,
include: /node_modules\/katex\/dist/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
name: '[name].[hash].[ext]'
}
},
],
},
{
test: /\.(eot|ttf|woff|woff2)$/,
include: /node_modules\/katex\/dist\/fonts/,
loader: 'file-loader',
options: {
name: '[name].[hash].[ext]',
}
},
{ {
test: /monaco-editor\/\w+\/vs\/loader\.js$/, test: /monaco-editor\/\w+\/vs\/loader\.js$/,
use: [ use: [
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class SchedulePopulateUntrackedUploadsIfNeeded < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
class UntrackedFile < ActiveRecord::Base
include EachBatch
self.table_name = 'untracked_files_for_uploads'
end
def up
if table_exists?(:untracked_files_for_uploads)
process_or_remove_table
end
end
def down
# nothing
end
private
def process_or_remove_table
if UntrackedFile.all.empty?
drop_temp_table
else
schedule_populate_untracked_uploads_jobs
end
end
def drop_temp_table
drop_table(:untracked_files_for_uploads, if_exists: true)
end
def schedule_populate_untracked_uploads_jobs
say "Scheduling #{FOLLOW_UP_MIGRATION} background migration jobs since there are rows in untracked_files_for_uploads."
bulk_queue_background_migration_jobs_by_range(
UntrackedFile, FOLLOW_UP_MIGRATION)
end
end
...@@ -20,7 +20,7 @@ class CleanupMoveSystemUploadFolderSymlink < ActiveRecord::Migration ...@@ -20,7 +20,7 @@ class CleanupMoveSystemUploadFolderSymlink < ActiveRecord::Migration
def down def down
if File.directory?(new_directory) if File.directory?(new_directory)
say "Symlinking #{old_directory} -> #{new_directory}" say "Symlinking #{old_directory} -> #{new_directory}"
FileUtils.ln_s(new_directory, old_directory) FileUtils.ln_s(new_directory, old_directory) unless File.exist?(old_directory)
else else
say "#{new_directory} doesn't exist, skipping." say "#{new_directory} doesn't exist, skipping."
end end
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180206200543) do ActiveRecord::Schema.define(version: 20180208183958) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -207,10 +207,39 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod ...@@ -207,10 +207,39 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod
var c = pureFunction(values.foo); var c = pureFunction(values.foo);
``` ```
1. Avoid constructors with side-effects 1. Avoid constructors with side-effects.
Although we aim for code without side-effects we need some side-effects for our code to run.
If the class won't do anything if we only instantiate it, it's ok to add side effects into the constructor (_Note:_ The following is just an example. If the only purpose of the class is to add an event listener and handle the callback a function will be more suitable.)
```javascript
// Bad
export class Foo {
constructor() {
this.init();
}
init() {
document.addEventListener('click', this.handleCallback)
},
handleCallback() {
}
}
// Good
export class Foo {
constructor() {
document.addEventListener()
}
handleCallback() {
}
}
```
On the other hand, if a class only needs to extend a third party/add event listeners in some specific cases, they should be initialized oustside of the constructor.
1. Prefer `.map`, `.reduce` or `.filter` over `.forEach` 1. Prefer `.map`, `.reduce` or `.filter` over `.forEach`
A forEach will cause side effects, it will be mutating the array being iterated. Prefer using `.map`, A forEach will most likely cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
`.reduce` or `.filter` `.reduce` or `.filter`
```javascript ```javascript
const users = [ { name: 'Foo' }, { name: 'Bar' } ]; const users = [ { name: 'Foo' }, { name: 'Bar' } ];
...@@ -302,20 +331,20 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ...@@ -302,20 +331,20 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
#### Naming #### Naming
1. **Extensions**: Use `.vue` extension for Vue components. 1. **Extensions**: Use `.vue` extension for Vue components.
1. **Reference Naming**: Use camelCase for their instances: 1. **Reference Naming**: Use PascalCase for their instances:
```javascript ```javascript
// bad // bad
import CardBoard from 'cardBoard' import cardBoard from 'cardBoard.vue'
components: { components: {
CardBoard: cardBoard,
}; };
// good // good
import cardBoard from 'cardBoard' import CardBoard from 'cardBoard.vue'
components: { components: {
cardBoard: CardBoard,
}; };
``` ```
......
...@@ -17,6 +17,9 @@ would be `process_something`. If you're not sure what queue a worker uses, ...@@ -17,6 +17,9 @@ would be `process_something`. If you're not sure what queue a worker uses,
you can find it using `SomeWorker.queue`. There is almost never a reason to you can find it using `SomeWorker.queue`. There is almost never a reason to
manually override the queue name using `sidekiq_options queue: :some_queue`. manually override the queue name using `sidekiq_options queue: :some_queue`.
You must always add any new queues to `app/workers/all_queues.yml` otherwise
your worker will not run.
## Queue Namespaces ## Queue Namespaces
While different workers cannot share a queue, they can share a queue namespace. While different workers cannot share a queue, they can share a queue namespace.
......
...@@ -85,9 +85,9 @@ module API ...@@ -85,9 +85,9 @@ module API
use :pagination use :pagination
end end
get ':id/-/search' do get ':id/-/search' do
find_group!(params[:id]) group = find_group!(params[:id])
present search(group_id: params[:id]), with: entity present search(group_id: group.id), with: entity
end end
end end
...@@ -106,9 +106,9 @@ module API ...@@ -106,9 +106,9 @@ module API
use :pagination use :pagination
end end
get ':id/-/search' do get ':id/-/search' do
find_project!(params[:id]) project = find_project!(params[:id])
present search(project_id: params[:id]), with: entity present search(project_id: project.id), with: entity
end end
end end
end end
......
...@@ -60,7 +60,7 @@ module API ...@@ -60,7 +60,7 @@ module API
end end
post ':id/mark_as_done' do post ':id/mark_as_done' do
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
todo = Todo.find(params[:id]) todo = current_user.todos.find(params[:id])
present todo, with: Entities::Todo, current_user: current_user present todo, with: Entities::Todo, current_user: current_user
end end
......
...@@ -12,7 +12,7 @@ module API ...@@ -12,7 +12,7 @@ module API
end end
delete ':id' do delete ':id' do
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
todo = Todo.find(params[:id]) todo = current_user.todos.find(params[:id])
present todo, with: ::API::Entities::Todo, current_user: current_user present todo, with: ::API::Entities::Todo, current_user: current_user
end end
......
...@@ -14,23 +14,33 @@ module Banzai ...@@ -14,23 +14,33 @@ module Banzai
end end
def highlight_node(node) def highlight_node(node)
code = node.text
css_classes = 'code highlight js-syntax-highlight' css_classes = 'code highlight js-syntax-highlight'
language = node.attr('lang') lang = node.attr('lang')
retried = false
if use_rouge?(language) if use_rouge?(lang)
lexer = lexer_for(language) lexer = lexer_for(lang)
language = lexer.tag language = lexer.tag
else
lexer = Rouge::Lexers::PlainText.new
language = lang
end
begin begin
code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: language) code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, node.text), tag: language)
css_classes << " #{language}" css_classes << " #{language}" if language
rescue rescue
# Gracefully handle syntax highlighter bugs/errors to ensure # Gracefully handle syntax highlighter bugs/errors to ensure users can
# users can still access an issue/comment/etc. # still access an issue/comment/etc. First, retry with the plain text
# filter. If that fails, then just skip this entirely, but that would
# be a pretty bad upstream bug.
return if retried
language = nil language = nil
end lexer = Rouge::Lexers::PlainText.new
retried = true
retry
end end
highlighted = %(<pre class="#{css_classes}" lang="#{language}" v-pre="true"><code>#{code}</code></pre>) highlighted = %(<pre class="#{css_classes}" lang="#{language}" v-pre="true"><code>#{code}</code></pre>)
......
...@@ -43,8 +43,12 @@ module Gitlab ...@@ -43,8 +43,12 @@ module Gitlab
store_untracked_file_paths store_untracked_file_paths
if UntrackedFile.all.empty?
drop_temp_table
else
schedule_populate_untracked_uploads_jobs schedule_populate_untracked_uploads_jobs
end end
end
private private
...@@ -92,7 +96,7 @@ module Gitlab ...@@ -92,7 +96,7 @@ module Gitlab
end end
end end
yield(paths) yield(paths) if paths.any?
end end
def build_find_command(search_dir) def build_find_command(search_dir)
...@@ -165,6 +169,11 @@ module Gitlab ...@@ -165,6 +169,11 @@ module Gitlab
bulk_queue_background_migration_jobs_by_range( bulk_queue_background_migration_jobs_by_range(
UntrackedFile, FOLLOW_UP_MIGRATION) UntrackedFile, FOLLOW_UP_MIGRATION)
end end
def drop_temp_table
UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
if_exists: true)
end
end end
end end
end end
...@@ -28,9 +28,9 @@ module Gitlab ...@@ -28,9 +28,9 @@ module Gitlab
# encode and clean the bad chars # encode and clean the bad chars
message.replace clean(message) message.replace clean(message)
rescue ArgumentError rescue ArgumentError => e
return nil return unless e.message.include?('unknown encoding name')
rescue
encoding = detect ? detect[:encoding] : "unknown" encoding = detect ? detect[:encoding] : "unknown"
"--broken encoding: #{encoding}" "--broken encoding: #{encoding}"
end end
......
...@@ -13,8 +13,6 @@ module Gitlab ...@@ -13,8 +13,6 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts') gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
gon.gitlab_url = Gitlab.config.gitlab.url gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION gon.revision = Gitlab::REVISION
......
...@@ -50,9 +50,10 @@ module Gitlab ...@@ -50,9 +50,10 @@ module Gitlab
end end
def wiki_restorer def wiki_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
shared: @shared, shared: @shared,
project: ProjectWiki.new(project_tree.restored_project)) project: ProjectWiki.new(project_tree.restored_project),
wiki_enabled: @project.wiki_enabled?)
end end
def uploads_restorer def uploads_restorer
......
module Gitlab
module ImportExport
class WikiRestorer < RepoRestorer
def initialize(project:, shared:, path_to_bundle:, wiki_enabled:)
super(project: project, shared: shared, path_to_bundle: path_to_bundle)
@wiki_enabled = wiki_enabled
end
def restore
@project.wiki if create_empty_wiki?
super
end
private
def create_empty_wiki?
!File.exist?(@path_to_bundle) && @wiki_enabled
end
end
end
end
...@@ -42,7 +42,7 @@ module Gitlab ...@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first key, value = parsed_field.first
if value.nil? if value.nil?
value = open_file(tmp_path) value = open_file(tmp_path, @request.params["#{key}.name"])
@open_files << value @open_files << value
else else
value = decorate_params_value(value, @request.params[key], tmp_path) value = decorate_params_value(value, @request.params[key], tmp_path)
...@@ -70,7 +70,7 @@ module Gitlab ...@@ -70,7 +70,7 @@ module Gitlab
case path_value case path_value
when nil when nil
value_hash[path_key] = open_file(tmp_path) value_hash[path_key] = open_file(tmp_path, value_hash.dig(path_key, '.name'))
@open_files << value_hash[path_key] @open_files << value_hash[path_key]
value_hash value_hash
when Hash when Hash
...@@ -81,8 +81,8 @@ module Gitlab ...@@ -81,8 +81,8 @@ module Gitlab
end end
end end
def open_file(path) def open_file(path, name)
::UploadedFile.new(path, File.basename(path), 'application/octet-stream') ::UploadedFile.new(path, name || File.basename(path), 'application/octet-stream')
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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