Commit 1f577b6a authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-02-02' into 'master'

CE upstream - 2018-02-02 18:24 UTC

See merge request gitlab-org/gitlab-ee!4366
parents 8f4b6e2e 822b1b3b
/* 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 { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash';
import flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
......@@ -42,12 +43,8 @@ export default class Issue {
this.disableCloseReopenButton($button);
url = $button.attr('href');
return $.ajax({
type: 'PUT',
url: url
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
return axios.put(url)
.then(({ data }) => {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
......@@ -74,9 +71,10 @@ export default class Issue {
}
}
} else {
new Flash(issueFailMessage);
flash(issueFailMessage);
}
})
.catch(() => flash(issueFailMessage))
.then(() => {
this.disableCloseReopenButton($button, false);
});
......@@ -115,24 +113,22 @@ export default class Issue {
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
return $.getJSON($container.data('url')).fail(function() {
return new Flash('Failed to load referenced merge requests');
}).done(function(data) {
if ('html' in data) {
return $container.html(data.html);
}
});
return axios.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {
$container.html(data.html);
}
}).catch(() => flash('Failed to load referenced merge requests'));
}
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
return $.getJSON($container.data('url')).fail(function() {
return new Flash('Failed to load related branches');
}).done(function(data) {
if ('html' in data) {
return $container.html(data.html);
}
});
return axios.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {
$container.html(data.html);
}
}).catch(() => flash('Failed to load related branches'));
}
}
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
......@@ -8,6 +9,7 @@ export default class Job {
constructor(options) {
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath;
......@@ -171,12 +173,23 @@ export default class Job {
}
getBuildTrace() {
return $.ajax({
url: `${this.pagePath}/trace.json`,
data: { state: this.state },
return axios.get(`${this.pagePath}/trace.json`, {
params: { state: this.state },
})
.done((log) => {
setCiStatusFavicon(`${this.pagePath}/status.json`);
.then((res) => {
const log = res.data;
if (!this.fetchingStatusFavicon) {
this.fetchingStatusFavicon = true;
setCiStatusFavicon(`${this.pagePath}/status.json`)
.then(() => {
this.fetchingStatusFavicon = false;
})
.catch(() => {
this.fetchingStatusFavicon = false;
});
}
if (log.state) {
this.state = log.state;
......@@ -217,7 +230,7 @@ export default class Job {
visitUrl(this.pagePath);
}
})
.fail(() => {
.catch(() => {
this.$buildRefreshAnimation.remove();
})
.then(() => {
......
......@@ -2,9 +2,12 @@
/* global Issuable */
/* global ListLabel */
import _ from 'underscore';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
import flash from './flash';
export default class LabelsSelect {
constructor(els, options = {}) {
......@@ -82,99 +85,96 @@ export default class LabelsSelect {
}
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
dataType: 'JSON',
data: data
}).done(function(data) {
var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueURLSplit = issueURLSplit;
labelCount = 0;
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
}
else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
axios.put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueURLSplit = issueURLSplit;
labelCount = 0;
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
}
else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) {
labelTitles = data.labels.map(function(label) {
return label.title;
});
if (data.labels.length) {
labelTitles = data.labels.map(function(label) {
return label.title;
});
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
labelTooltipTitle = labelTitles.join(', ');
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
labelTooltipTitle = labelTitles.join(', ');
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
.tooltip('fixTitle');
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
.tooltip('fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body'
});
});
$('.has-tooltip', $value).tooltip({
container: 'body'
});
})
.catch(() => flash(__('Error saving label update.')));
};
$dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: labelUrl
}).done(function(data) {
data = _.chain(data).groupBy(function(label) {
return label.title;
}).map(function(label) {
var color;
color = _.map(label, function(dup) {
return dup.color;
});
return {
id: label[0].id,
title: label[0].title,
color: color,
duplicate: color.length > 1
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
title: 'No Label'
axios.get(labelUrl)
.then((res) => {
let data = _.chain(res.data).groupBy(function(label) {
return label.title;
}).map(function(label) {
var color;
color = _.map(label, function(dup) {
return dup.color;
});
return {
id: label[0].id,
title: label[0].title,
color: color,
duplicate: color.length > 1
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
title: 'No Label'
});
}
if (showAny) {
extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
if (extraData.length) {
extraData.push('divider');
data = extraData.concat(data);
}
}
if (showAny) {
extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
if (extraData.length) {
extraData.push('divider');
data = extraData.concat(data);
}
}
callback(data);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
});
callback(data);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
})
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
......
import axios from './axios_utils';
import Cache from './cache';
class AjaxCache extends Cache {
......@@ -18,25 +19,18 @@ class AjaxCache extends Cache {
let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) {
pendingRequest = new Promise((resolve, reject) => {
// jQuery 2 is not Promises/A+ compatible (missing catch)
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data),
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${endpoint}: ${errorThrown}`);
error.textStatus = textStatus;
reject(error);
},
);
})
.then((data) => {
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((error) => {
delete this.pendingRequests[endpoint];
throw error;
});
pendingRequest = axios.get(endpoint)
.then(({ data }) => {
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((e) => {
const error = new Error(`${endpoint}: ${e.message}`);
error.textStatus = e.message;
delete this.pendingRequests[endpoint];
throw error;
});
this.pendingRequests[endpoint] = pendingRequest;
}
......
......@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => {
window.activeVueResources -= 1;
return config;
}, (e) => {
window.activeVueResources -= 1;
return Promise.reject(e);
});
export default axios;
......
import { getLocationHash } from './url_utility';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
......@@ -28,16 +28,11 @@ export const isInIssuePage = () => {
return page === 'issues' && action === 'show';
};
export const ajaxGet = url => $.ajax({
type: 'GET',
url,
dataType: 'script',
});
export const ajaxPost = (url, data) => $.ajax({
type: 'POST',
url,
data,
export const ajaxGet = url => axios.get(url, {
params: { format: 'js' },
responseType: 'text',
}).then(({ data }) => {
$.globalEval(data);
});
export const rstrip = (val) => {
......@@ -412,7 +407,6 @@ window.gl.utils = {
getGroupSlug,
isInIssuePage,
ajaxGet,
ajaxPost,
rstrip,
updateTooltipTitle,
disableButtonIfEmptyField,
......
/* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
......@@ -10,20 +11,11 @@
}
fetchConflictsData() {
return $.ajax({
dataType: 'json',
url: this.conflictsPath
});
return axios.get(this.conflictsPath);
}
submitResolveConflicts(data) {
return $.ajax({
url: this.resolveConflictsPath,
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
method: 'POST'
});
return axios.post(this.resolveConflictsPath, data);
}
}
......
......@@ -38,24 +38,23 @@ $(() => {
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
},
created() {
mergeConflictsService
.fetchConflictsData()
.done((data) => {
mergeConflictsService.fetchConflictsData()
.then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
} else {
mergeConflictsStore.setConflictsData(data);
}
})
.error(() => {
mergeConflictsStore.setFailedRequest();
})
.always(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
syntaxHighlight($('.js-syntax-highlight'));
});
})
.catch(() => {
mergeConflictsStore.setLoadingState(false);
mergeConflictsStore.setFailedRequest();
});
},
methods: {
......@@ -82,10 +81,10 @@ $(() => {
mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData())
.done((data) => {
.then(({ data }) => {
window.location.href = data.redirect_to;
})
.error(() => {
.catch(() => {
mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!');
});
......
/* eslint-disable no-new, class-methods-use-this */
import Cookies from 'js-cookie';
import Flash from './flash';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
......@@ -244,15 +245,22 @@ export default class MergeRequestTabs {
if (this.commitsLoaded) {
return;
}
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
this.toggleLoading(true);
axios.get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits'));
this.commitsLoaded = true;
this.scrollToElement('#commits');
},
});
this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
}
mountPipelinesView() {
......@@ -283,9 +291,10 @@ export default class MergeRequestTabs {
// some pages like MergeRequestsController#new has query parameters on that anchor
const urlPathname = parseUrlPathname(source);
this.ajaxGet({
url: `${urlPathname}.json${location.search}`,
success: (data) => {
this.toggleLoading(true);
axios.get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
......@@ -335,8 +344,13 @@ export default class MergeRequestTabs {
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
}
},
});
this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
}
// Show or hide the loading spinner
......@@ -346,17 +360,6 @@ export default class MergeRequestTabs {
$('.mr-loading-status .loading').toggle(status);
}
ajaxGet(options) {
const defaults = {
beforeSend: () => this.toggleLoading(true),
error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
};
$.ajax($.extend({}, defaults, options));
}
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
......
import Flash from './flash';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class Milestone {
constructor() {
......@@ -33,15 +34,12 @@ export default class Milestone {
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
axios.get(endpoint)
.then(({ data }) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
})
.catch(() => flash('Error loading milestone tab'));
}
}
}
......@@ -2,6 +2,7 @@
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
export default class MilestoneSelect {
......@@ -52,48 +53,47 @@ export default class MilestoneSelect {
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: (term, callback) => $.ajax({
url: milestonesUrl
}).done((data) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
data: (term, callback) => axios.get(milestonesUrl)
.then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}),
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
......@@ -200,26 +200,23 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data
}).done((data) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
return axios.put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
}
}
});
......
......@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
......@@ -1399,7 +1399,7 @@ export default class Notes {
* 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
* 3) Build temporary placeholder element (using `createPlaceholderNote`)
* 4) Show placeholder note on UI
* 5) Perform network request to submit the note using `ajaxPost`
* 5) Perform network request to submit the note using `axios.post`
* a) If request is successfully completed
* 1. Remove placeholder element
* 2. Show submitted Note element
......@@ -1481,8 +1481,10 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
ajaxPost(formAction, formData)
.then((note) => {
axios.post(formAction, formData)
.then((res) => {
const note = res.data;
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
......@@ -1555,7 +1557,7 @@ export default class Notes {
}
$form.trigger('ajax:success', [note]);
}).fail(() => {
}).catch(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
......@@ -1594,7 +1596,7 @@ export default class Notes {
*
* 1) Get Form metadata
* 2) Update note element with new content
* 3) Perform network request to submit the updated note using `ajaxPost`
* 3) Perform network request to submit the updated note using `axios.post`
* a) If request is successfully completed
* 1. Show submitted Note element
* b) If request failed
......@@ -1625,12 +1627,12 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
ajaxPost(formAction, formData)
.then((note) => {
axios.post(formAction, formData)
.then(({ data }) => {
// Submission successful! render final note element
this.updateNote(note, $editingNote);
this.updateNote(data, $editingNote);
})
.fail(() => {
.catch(() => {
// Submission failed, revert back to original note
$noteBodyText.html(_.escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in');
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie';
import { visitUrl } from '../../lib/utils/url_utility';
import { __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import projectSelect from '../../project_select';
export default class Project {
......@@ -79,17 +82,15 @@ export default class Project {
$dropdown = $(this);
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
data(term, callback) {
axios.get($dropdown.data('refs-url'), {
params: {
ref: $dropdown.data('ref'),
search: term,
},
dataType: 'json',
}).done(function(refs) {
return callback(refs);
});
})
.then(({ data }) => callback(data))
.catch(() => flash(__('An error occurred while getting projects')));
},
selectable: true,
filterable: true,
......
......@@ -48,7 +48,7 @@
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<ul class="nav-links scrolling-tabs separator">
<li
v-for="(tab, i) in tabs"
:key="i"
......
......@@ -82,6 +82,10 @@
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) {
width: 100%;
&.mobile-separator {
border-bottom: 1px solid $border-color;
}
}
}
......@@ -168,9 +172,9 @@
display: inline-block;
}
// Applies on /dashboard/issues
.project-item-select-holder {
margin: 0;
width: 100%;
}
&.inline {
......@@ -367,7 +371,6 @@
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
float: right;
......
......@@ -91,6 +91,10 @@ module MergeRequests
merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
# Upcoming method calls need the refreshed version of
# @source_merge_requests diffs (for MergeRequest#commit_shas for instance).
merge_requests_for_source_branch(reload: true)
end
# Note: Closed merge requests also need approvals reset.
......@@ -212,7 +216,8 @@ module MergeRequests
merge_requests.uniq.select(&:source_project)
end
def merge_requests_for_source_branch
def merge_requests_for_source_branch(reload: false)
@source_merge_requests = nil if reload
@source_merge_requests ||= merge_requests_for(@branch_name)
end
......
class AttachmentUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads
include UploaderHelper
private
......
......@@ -8,11 +8,11 @@ class AvatarUploader < GitlabUploader
model.avatar.file && model.avatar.file.present?
end
def move_to_store
def move_to_cache
false
end
def move_to_cache
def move_to_store
false
end
......
......@@ -15,8 +15,6 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
attr_accessor :model
def self.root
File.join(options.storage_path, 'uploads')
end
......@@ -62,6 +60,8 @@ class FileUploader < GitlabUploader
SecureRandom.hex
end
attr_accessor :model
def initialize(model, secret = nil)
@model = model
@secret = secret
......
......@@ -45,6 +45,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
file.present?
end
def store_dir
File.join(base_dir, dynamic_segment)
end
def cache_dir
File.join(root, base_dir, 'tmp/cache')
end
......@@ -62,10 +66,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
# Designed to be overridden by child uploaders that have a dynamic path
# segment -- that is, a path that changes based on mutable attributes of its
# associated model
#
# For example, `FileUploader` builds the storage path based on the associated
# project model's `path_with_namespace` value, which can change when the
# project or its containing namespace is moved or renamed.
def dynamic_segment
raise(NotImplementedError)
end
......
.top-area
%ul.nav-links
%ul.nav-links.mobile-separator
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do
Your groups
......
......@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs
%ul.nav-links.scrolling-tabs.mobile-separator
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
......
.nav-block
%ul.nav-links
%ul.nav-links.mobile-separator
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
......
......@@ -4,7 +4,7 @@
- if current_user.todos.any?
.top-area
%ul.nav-links
%ul.nav-links.mobile-separator
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
......
......@@ -15,7 +15,7 @@
.col-sm-7.brand-holder.pull-left
%h1
= brand_title
= brand_image
= brand_image
- if brand_item&.description?
= brand_text
- else
......
%ul.nav-links
%ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All")
......
- failed_builds = @pipeline.statuses.latest.failed
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
%li.js-pipeline-tab-link
= link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
Pipeline
......
%ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- if event_filter_visible(:issues)
= event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
- if comments_visible?
= event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
= event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- if event_filter_visible(:issues)
= event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
- if comments_visible?
= event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
= event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
%ul.nav-links
%ul.nav-links.mobile-separator
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
......
%ul.nav-links
%ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
......
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
%ul.nav-links.issues-state-filters
%ul.nav-links.issues-state-filters.mobile-separator
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)}
......
- subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu
.nav-links.snippet-scope-menu.mobile-separator
%li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
All
......
---
title: Add unique constraint to trending_projects#project_id.
merge_request: 16846
author:
type: other
---
title: Fix GitLab import leaving group_id on ProjectLabel
merge_request: 16877
author:
type: fixed
---
title: Change button group width on mobile
merge_request: 16726
author: George Tsiolis
type: fixed
---
title: Reload MRs memoization after diffs creation
merge_request:
author:
type: fixed
......@@ -193,6 +193,12 @@ production: &base
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Uploads (attachments, avatars, etc...)
uploads:
# The location where uploads objects are stored (default: public/).
# storage_path: public/
# base_dir: uploads/-/system
## GitLab Pages
pages:
enabled: false
......
......@@ -346,7 +346,6 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values
# Settings.artifact['path'] is deprecated, use `storage_path` instead
Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings.artifacts['object_store'] ||= Settingslogic.new({})
Settings.artifacts['object_store']['enabled'] ||= false
Settings.artifacts['object_store']['remote_directory'] ||= nil
......@@ -413,6 +412,13 @@ Settings.uploads['object_store']['background_upload'] ||= true
# Convert upload connection settings to use string keys, to make Fog happy
Settings.uploads['object_store']['connection']&.deep_stringify_keys!
#
# Uploads
#
Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
#
# Mattermost
#
......
class AddUniqueConstraintToTrendingProjectsProjectId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :trending_projects, :project_id, unique: true, name: 'index_trending_projects_on_project_id_unique'
remove_concurrent_index_by_name :trending_projects, 'index_trending_projects_on_project_id'
rename_index :trending_projects, 'index_trending_projects_on_project_id_unique', 'index_trending_projects_on_project_id'
end
def down
rename_index :trending_projects, 'index_trending_projects_on_project_id', 'index_trending_projects_on_project_id_old'
add_concurrent_index :trending_projects, :project_id
remove_concurrent_index_by_name :trending_projects, 'index_trending_projects_on_project_id_old'
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectLabelsGroupId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
update_column_in_batches(:labels, :group_id, nil) do |table, query|
query.where(table[:type].eq('ProjectLabel').and(table[:group_id].not_eq(nil)))
end
end
def down
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180201145907) do
ActiveRecord::Schema.define(version: 20180202111106) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -2228,7 +2228,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do
t.integer "project_id", null: false
end
add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree
add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", unique: true, using: :btree
create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate"
......
......@@ -139,13 +139,12 @@ module Gitlab
end
def setup_label
return unless @relation_hash['type'] == 'GroupLabel'
# If there's no group, move the label to a project label
if @relation_hash['group_id']
if @relation_hash['type'] == 'GroupLabel' && @relation_hash['group_id']
@relation_hash['project_id'] = nil
@relation_name = :group_label
else
@relation_hash['group_id'] = nil
@relation_hash['type'] = 'ProjectLabel'
end
end
......
/* global BoardService */
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/labels_select';
import LabelsSelect from '~/boards/components/labels_select.vue';
import IssuableContext from '~/issuable_context';
......@@ -32,14 +34,16 @@ const label2 = {
};
describe('LabelsSelect', () => {
let mock;
beforeEach((done) => {
setFixtures('<div class="test-container"></div>');
const deferred = new jQuery.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.resolve([
mock = new MockAdapter(axios);
mock.onGet('/some/path').reply(200, [
label,
label2,
]));
]);
// eslint-disable-next-line no-new
new IssuableContext();
......@@ -60,6 +64,10 @@ describe('LabelsSelect', () => {
Vue.nextTick(done);
});
afterEach(() => {
mock.restore();
});
describe('canEdit', () => {
it('hides Edit button', (done) => {
vm.canEdit = false;
......
/* global BoardService */
import Vue from 'vue';
import MockAdapater from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import MilestoneSelect from '~/boards/components/milestone_select.vue';
import IssuableContext from '~/issuable_context';
import { boardObj } from './mock_data';
......@@ -92,12 +94,18 @@ describe('Milestone select component', () => {
});
describe('clicking dropdown items', () => {
let mock;
beforeEach(() => {
const deferred = new jQuery.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.resolve([
mock = new MockAdapater(axios);
mock.onGet('/test/issue-boards/milestones.json').reply(200, [
milestone,
milestone2,
]));
]);
});
afterEach(() => {
mock.restore();
});
it('sets Any Milestone', (done) => {
......
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Issue from '~/issue';
import '~/lib/utils/text_utility';
......@@ -88,20 +90,24 @@ describe('Issue', function() {
[true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
const action = isIssueInitiallyOpen ? 'close' : 'reopen';
let mock;
function ajaxSpy(req) {
if (req.url === this.$triggeredButton.attr('href')) {
expect(req.type).toBe('PUT');
function mockCloseButtonResponseSuccess(url, response) {
mock.onPut(url).reply(() => {
expectNewBranchButtonState(true, false);
return this.issueStateDeferred;
} else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
expect(req.type).toBe('GET');
expectNewBranchButtonState(true, false);
return this.canCreateBranchDeferred;
}
expect(req.url).toBe('unexpected');
return null;
return [200, response];
});
}
function mockCloseButtonResponseError(url) {
mock.onPut(url).networkError();
}
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
});
}
beforeEach(function() {
......@@ -111,6 +117,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw');
}
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen);
......@@ -120,71 +131,89 @@ describe('Issue', function() {
this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001');
this.issueStateDeferred = new jQuery.Deferred();
this.canCreateBranchDeferred = new jQuery.Deferred();
spyOn(axios, 'get').and.callThrough();
});
spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
afterEach(() => {
mock.restore();
$('div.flash-alert').remove();
});
it(`${action}s the issue`, function() {
this.$triggeredButton.trigger('click');
this.issueStateDeferred.resolve({
it(`${action}s the issue`, function(done) {
mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
id: 34
});
this.canCreateBranchDeferred.resolve({
can_create_branch: !isIssueInitiallyOpen
});
mockCanCreateBranch(!isIssueInitiallyOpen);
expectIssueState(!isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
expectNewBranchButtonState(false, !isIssueInitiallyOpen);
this.$triggeredButton.trigger('click');
setTimeout(() => {
expectIssueState(!isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
expectNewBranchButtonState(false, !isIssueInitiallyOpen);
done();
});
});
it(`fails to ${action} the issue if saved:false`, function() {
this.$triggeredButton.trigger('click');
this.issueStateDeferred.resolve({
it(`fails to ${action} the issue if saved:false`, function(done) {
mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
saved: false
});
this.canCreateBranchDeferred.resolve({
can_create_branch: isIssueInitiallyOpen
});
mockCanCreateBranch(isIssueInitiallyOpen);
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
this.$triggeredButton.trigger('click');
setTimeout(() => {
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
done();
});
});
it(`fails to ${action} the issue if HTTP error occurs`, function() {
it(`fails to ${action} the issue if HTTP error occurs`, function(done) {
mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
mockCanCreateBranch(isIssueInitiallyOpen);
this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
this.canCreateBranchDeferred.resolve({
can_create_branch: isIssueInitiallyOpen
});
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
setTimeout(() => {
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
done();
});
});
it('disables the new branch button if Ajax call fails', function() {
mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
mock.onGet(/(.*)\/can_create_branch$/).networkError();
this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
this.canCreateBranchDeferred.reject();
expectNewBranchButtonState(false, false);
});
it('does not trigger Ajax call if new branch button is missing', function() {
it('does not trigger Ajax call if new branch button is missing', function(done) {
mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
Issue.$btnNewBranch = $();
this.canCreateBranchDeferred = null;
this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
setTimeout(() => {
expect(axios.get).not.toHaveBeenCalled();
done();
});
});
});
});
......
This diff is collapsed.
/* eslint-disable no-new */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
......@@ -10,35 +12,44 @@ import '~/users_select';
(() => {
let saveLabelCount = 0;
let mock;
describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => {
loadFixtures('static/issue_sidebar_label.html.raw');
mock = new MockAdapter(axios);
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
spyOn(jQuery, 'ajax').and.callFake((req) => {
const d = $.Deferred();
let LABELS_DATA = [];
mock.onGet('/root/test/labels.json').reply(() => {
const labels = Array(10).fill().map((_, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
}));
if (req.url === '/root/test/labels.json') {
for (let i = 0; i < 10; i += 1) {
LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
} else if (req.url === '/root/test/issues/2.json') {
const tmp = [];
for (let i = 0; i < saveLabelCount; i += 1) {
tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
LABELS_DATA = { labels: tmp };
}
return [200, labels];
});
mock.onPut('/root/test/issues/2.json').reply(() => {
const labels = Array(saveLabelCount).fill().map((_, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
}));
d.resolve(LABELS_DATA);
return d.promise();
return [200, { labels }];
});
});
afterEach(() => {
mock.restore();
});
it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5;
$('.edit-link').get(0).click();
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import AjaxCache from '~/lib/utils/ajax_cache';
describe('AjaxCache', () => {
......@@ -87,66 +89,53 @@ describe('AjaxCache', () => {
});
describe('retrieve', () => {
let ajaxSpy;
let mock;
beforeEach(() => {
spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
mock = new MockAdapter(axios);
spyOn(axios, 'get').and.callThrough();
});
afterEach(() => {
mock.restore();
});
it('stores and returns data from Ajax call if cache is empty', (done) => {
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.resolve(dummyResponse);
return deferred.promise();
};
mock.onGet(dummyEndpoint).reply(200, dummyResponse);
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
expect(data).toBe(dummyResponse);
expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse);
expect(data).toEqual(dummyResponse);
expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse);
})
.then(done)
.catch(fail);
});
it('makes no Ajax call if request is pending', () => {
const responseDeferred = $.Deferred();
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
// neither reject nor resolve to keep request pending
return responseDeferred.promise();
};
const unexpectedResponse = data => fail(`Did not expect response: ${data}`);
it('makes no Ajax call if request is pending', (done) => {
mock.onGet(dummyEndpoint).reply(200, dummyResponse);
AjaxCache.retrieve(dummyEndpoint)
.then(unexpectedResponse)
.then(done)
.catch(fail);
AjaxCache.retrieve(dummyEndpoint)
.then(unexpectedResponse)
.then(done)
.catch(fail);
expect($.ajax.calls.count()).toBe(1);
expect(axios.get.calls.count()).toBe(1);
});
it('returns undefined if Ajax call fails and cache is empty', (done) => {
const dummyStatusText = 'exploded';
const dummyErrorMessage = 'server exploded';
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.reject(null, dummyStatusText, dummyErrorMessage);
return deferred.promise();
};
const errorMessage = 'Network Error';
mock.onGet(dummyEndpoint).networkError();
AjaxCache.retrieve(dummyEndpoint)
.then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`))
.catch((error) => {
expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`);
expect(error.textStatus).toBe(dummyStatusText);
expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`);
expect(error.textStatus).toBe(errorMessage);
done();
})
.catch(fail);
......@@ -154,7 +143,9 @@ describe('AjaxCache', () => {
it('makes no Ajax call if matching data exists', (done) => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
ajaxSpy = () => fail(new Error('expected no Ajax call!'));
mock.onGet(dummyEndpoint).reply(() => {
fail(new Error('expected no Ajax call!'));
});
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
......@@ -171,12 +162,7 @@ describe('AjaxCache', () => {
AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse;
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.resolve(dummyResponse);
return deferred.promise();
};
mock.onGet(dummyEndpoint).reply(200, dummyResponse);
// Call without forceRetrieve param
AjaxCache.retrieve(dummyEndpoint)
......@@ -189,7 +175,7 @@ describe('AjaxCache', () => {
// Call with forceRetrieve param
AjaxCache.retrieve(dummyEndpoint, true)
.then((data) => {
expect(data).toBe(dummyResponse);
expect(data).toEqual(dummyResponse);
})
.then(done)
.catch(fail);
......
/* eslint-disable promise/catch-or-return */
import * as commonUtils from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter';
describe('common_utils', () => {
......@@ -460,17 +459,6 @@ describe('common_utils', () => {
});
});
describe('ajaxPost', () => {
it('should perform `$.ajax` call and do `POST` request', () => {
const requestURL = '/some/random/api';
const data = { keyname: 'value' };
const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
commonUtils.ajaxPost(requestURL, data);
expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
});
});
describe('spriteIcon', () => {
let beforeGon;
......
/* eslint-disable no-var, comma-dangle, object-shorthand */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
......@@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo';
describe('activateTab', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
this.subject = this.class.activateTab;
});
......@@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo';
describe('setCurrentAction', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
this.subject = this.class.setCurrentAction;
});
......@@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo';
});
describe('tabShown', () => {
let mock;
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function (options) {
options.success({ html: '' });
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/diffs\.json/).reply(200, {
data: { html: '' },
});
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
});
afterEach(() => {
mock.restore();
});
describe('with "Side-by-side"/parallel diff view', () => {
beforeEach(function () {
this.class.diffViewType = () => 'parallel';
......@@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo';
it('triggers Ajax request to JSON endpoint', function (done) {
const url = '/foo/bar/merge_requests/1/diffs';
spyOn(this.class, 'ajaxGet').and.callFake((options) => {
expect(options.url).toEqual(`${url}.json`);
spyOn(axios, 'get').and.callFake((reqUrl) => {
expect(reqUrl).toBe(`${url}.json`);
done();
return Promise.resolve({ data: {} });
});
this.class.loadDiff(url);
});
it('triggers scroll event when diff already loaded', function (done) {
spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail());
spyOn(axios, 'get').and.callFake(done.fail);
spyOn(document, 'dispatchEvent');
this.class.diffsLoaded = true;
......@@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo';
describe('with inline diff', () => {
let noteId;
let noteLineNumId;
let mock;
beforeEach(() => {
const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture);
......@@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo';
.attr('href')
.replace('#', '');
spyOn($, 'ajax').and.callFake(function (options) {
options.success(diffsResponse);
});
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
});
afterEach(() => {
mock.restore();
});
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function () {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0);
expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
target: jasmine.any(Object),
lineType: 'old',
forceShow: true,
setTimeout(() => {
expect(noteId.length).toBeGreaterThan(0);
expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
target: jasmine.any(Object),
lineType: 'old',
forceShow: true,
});
done();
});
});
it('should gracefully ignore non-existant fragment hash', function () {
it('should gracefully ignore non-existant fragment hash', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
setTimeout(() => {
expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
done();
});
});
});
......@@ -370,6 +395,7 @@ import 'vendor/jquery.scrollTo';
describe('with parallel diff', () => {
let noteId;
let noteLineNumId;
let mock;
beforeEach(() => {
const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture);
......@@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo';
.attr('href')
.replace('#', '');
spyOn($, 'ajax').and.callFake(function (options) {
options.success(diffsResponse);
});
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
});
afterEach(() => {
mock.restore();
});
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function () {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0);
expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
target: jasmine.any(Object),
lineType: 'new',
forceShow: true,
setTimeout(() => {
expect(noteId.length).toBeGreaterThan(0);
expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
target: jasmine.any(Object),
lineType: 'new',
forceShow: true,
});
done();
});
});
it('should gracefully ignore non-existant fragment hash', function () {
it('should gracefully ignore non-existant fragment hash', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
setTimeout(() => {
expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
done();
});
});
});
......
This diff is collapsed.
......@@ -236,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
labels = project.issues.first.labels
expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
end
end
shared_examples 'restores group correctly' do |**results|
it 'has group label' do
expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
expect(project.group.labels.where(type: "GroupLabel").where.not(project_id: nil).count).to eq(0)
end
it 'has group milestone' do
......
# encoding: utf-8
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_labels_group_id.rb')
describe RemoveProjectLabelsGroupId, :delete do
let(:migration) { described_class.new }
let(:group) { create(:group) }
let!(:project_label) { create(:label, group_id: group.id) }
let!(:group_label) { create(:group_label) }
describe '#up' do
it 'updates the project labels group ID' do
expect { migration.up }.to change { project_label.reload.group_id }.to(nil)
end
it 'keeps the group labels group ID' do
expect { migration.up }.not_to change { group_label.reload.group_id }
end
end
end
......@@ -80,6 +80,14 @@ describe MergeRequests::RefreshService do
expect(@fork_merge_request.approvals).not_to be_empty
end
it 'reloads source branch MRs memoization' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }.to change {
refresh_service.instance_variable_get("@source_merge_requests").first.merge_request_diff
}
end
context 'when source branch ref does not exists' do
before do
DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
......@@ -486,37 +494,21 @@ describe MergeRequests::RefreshService do
end
it 'references the commit that caused the Work in Progress status' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
allow(refresh_service).to receive(:find_new_commits)
refresh_service.instance_variable_set("@commits", [
double(
id: 'aaaaaaa',
sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e',
short_id: 'aaaaaaa',
title: 'Fix issue',
work_in_progress?: false
),
double(
id: 'bbbbbbb',
sha: '498214de67004b1da3d820901307bed2a68a8ef6',
short_id: 'bbbbbbb',
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'bbbbbbb'
),
double(
id: 'ccccccc',
sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141',
short_id: 'ccccccc',
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'ccccccc'
)
])
refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
reload_mrs
expect(@merge_request.notes.last.note).to eq(
"marked as a **Work In Progress** from bbbbbbb"
wip_merge_request = create(:merge_request,
source_project: @project,
source_branch: 'wip',
target_branch: 'master',
target_project: @project)
commits = wip_merge_request.commits
oldrev = commits.last.id
newrev = commits.first.id
wip_commit = wip_merge_request.commits.find(&:work_in_progress?)
refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
expect(wip_merge_request.reload.notes.last.note).to eq(
"marked as a **Work In Progress** from #{wip_commit.id}"
)
end
......
shared_examples "matches the method pattern" do |method|
let(:target) { subject }
let(:args) { nil }
let(:pattern) { patterns[method] }
it do
return skip "No pattern provided, skipping." unless pattern
expect(target.method(method).call(*args)).to match(pattern)
end
end
shared_examples "builds correct paths" do |**patterns|
let(:patterns) { patterns }
before do
allow(subject).to receive(:filename).and_return('<filename>')
end
describe "#store_dir" do
it_behaves_like "matches the method pattern", :store_dir
end
describe "#cache_dir" do
it_behaves_like "matches the method pattern", :cache_dir
end
describe "#work_dir" do
it_behaves_like "matches the method pattern", :work_dir
end
describe "#upload_path" do
it_behaves_like "matches the method pattern", :upload_path
end
describe ".absolute_path" do
it_behaves_like "matches the method pattern", :absolute_path do
let(:target) { subject.class }
let(:args) { [upload] }
end
end
describe ".base_dir" do
it_behaves_like "matches the method pattern", :base_dir do
let(:target) { subject.class }
end
end
end
require 'spec_helper'
describe AvatarUploader do
let(:model) { build_stubbed(:user) }
let(:model) { create(:user, :with_avatar) }
let(:uploader) { described_class.new(model, :avatar) }
let(:upload) { create(:upload, model: model) }
......
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