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 */ /* 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 'vendor/jquery.waitforimages';
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';
import TaskList from './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper'; import IssuablesHelper from './helpers/issuables_helper';
...@@ -42,12 +43,8 @@ export default class Issue { ...@@ -42,12 +43,8 @@ export default class Issue {
this.disableCloseReopenButton($button); this.disableCloseReopenButton($button);
url = $button.attr('href'); url = $button.attr('href');
return $.ajax({ return axios.put(url)
type: 'PUT', .then(({ data }) => {
url: url
})
.fail(() => new Flash(issueFailMessage))
.done((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');
...@@ -74,9 +71,10 @@ export default class Issue { ...@@ -74,9 +71,10 @@ export default class Issue {
} }
} }
} else { } else {
new Flash(issueFailMessage); flash(issueFailMessage);
} }
}) })
.catch(() => flash(issueFailMessage))
.then(() => { .then(() => {
this.disableCloseReopenButton($button, false); this.disableCloseReopenButton($button, false);
}); });
...@@ -115,24 +113,22 @@ export default class Issue { ...@@ -115,24 +113,22 @@ export default class Issue {
static initMergeRequests() { static initMergeRequests() {
var $container; var $container;
$container = $('#merge-requests'); $container = $('#merge-requests');
return $.getJSON($container.data('url')).fail(function() { return axios.get($container.data('url'))
return new Flash('Failed to load referenced merge requests'); .then(({ data }) => {
}).done(function(data) { if ('html' in data) {
if ('html' in data) { $container.html(data.html);
return $container.html(data.html); }
} }).catch(() => flash('Failed to load referenced merge requests'));
});
} }
static initRelatedBranches() { static initRelatedBranches() {
var $container; var $container;
$container = $('#related-branches'); $container = $('#related-branches');
return $.getJSON($container.data('url')).fail(function() { return axios.get($container.data('url'))
return new Flash('Failed to load related branches'); .then(({ data }) => {
}).done(function(data) { if ('html' in data) {
if ('html' in data) { $container.html(data.html);
return $container.html(data.html); }
} }).catch(() => flash('Failed to load related branches'));
});
} }
} }
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints'; import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils'; import { numberToHumanSize } from './lib/utils/number_utils';
...@@ -8,6 +9,7 @@ export default class Job { ...@@ -8,6 +9,7 @@ export default class Job {
constructor(options) { constructor(options) {
this.timeout = null; this.timeout = null;
this.state = null; this.state = null;
this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath; this.pagePath = this.options.pagePath;
...@@ -171,12 +173,23 @@ export default class Job { ...@@ -171,12 +173,23 @@ export default class Job {
} }
getBuildTrace() { getBuildTrace() {
return $.ajax({ return axios.get(`${this.pagePath}/trace.json`, {
url: `${this.pagePath}/trace.json`, params: { state: this.state },
data: { state: this.state },
}) })
.done((log) => { .then((res) => {
setCiStatusFavicon(`${this.pagePath}/status.json`); 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) { if (log.state) {
this.state = log.state; this.state = log.state;
...@@ -217,7 +230,7 @@ export default class Job { ...@@ -217,7 +230,7 @@ export default class Job {
visitUrl(this.pagePath); visitUrl(this.pagePath);
} }
}) })
.fail(() => { .catch(() => {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
}) })
.then(() => { .then(() => {
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
/* global Issuable */ /* global Issuable */
/* global ListLabel */ /* global ListLabel */
import _ from 'underscore'; import _ from 'underscore';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils'; import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
import flash from './flash';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els, options = {}) { constructor(els, options = {}) {
...@@ -82,99 +85,96 @@ export default class LabelsSelect { ...@@ -82,99 +85,96 @@ export default class LabelsSelect {
} }
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ axios.put(issueUpdateURL, data)
type: 'PUT', .then(({ data }) => {
url: issueUpdateURL, var labelCount, template, labelTooltipTitle, labelTitles;
dataType: 'JSON', $loading.fadeOut();
data: data $dropdown.trigger('loaded.gl.dropdown');
}).done(function(data) { $selectbox.hide();
var labelCount, template, labelTooltipTitle, labelTitles; data.issueURLSplit = issueURLSplit;
$loading.fadeOut(); labelCount = 0;
$dropdown.trigger('loaded.gl.dropdown'); if (data.labels.length) {
$selectbox.hide(); template = labelHTMLTemplate(data);
data.issueURLSplit = issueURLSplit; labelCount = data.labels.length;
labelCount = 0; }
if (data.labels.length) { else {
template = labelHTMLTemplate(data); template = labelNoneHTMLTemplate;
labelCount = data.labels.length; }
} $value.removeAttr('style').html(template);
else { $sidebarCollapsedValue.text(labelCount);
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) { if (data.labels.length) {
labelTitles = data.labels.map(function(label) { labelTitles = data.labels.map(function(label) {
return label.title; return label.title;
}); });
if (labelTitles.length > 5) { if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5); labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more'); labelTitles.push('and ' + (data.labels.length - 5) + ' more');
} }
labelTooltipTitle = labelTitles.join(', '); labelTooltipTitle = labelTitles.join(', ');
} }
else { else {
labelTooltipTitle = ''; labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy'); $sidebarLabelTooltip.tooltip('destroy');
} }
$sidebarLabelTooltip $sidebarLabelTooltip
.attr('title', labelTooltipTitle) .attr('title', labelTooltipTitle)
.tooltip('fixTitle'); .tooltip('fixTitle');
$('.has-tooltip', $value).tooltip({ $('.has-tooltip', $value).tooltip({
container: 'body' container: 'body'
}); });
}); })
.catch(() => flash(__('Error saving label update.')));
}; };
$dropdown.glDropdown({ $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ axios.get(labelUrl)
url: labelUrl .then((res) => {
}).done(function(data) { let data = _.chain(res.data).groupBy(function(label) {
data = _.chain(data).groupBy(function(label) { return label.title;
return label.title; }).map(function(label) {
}).map(function(label) { var color;
var color; color = _.map(label, function(dup) {
color = _.map(label, function(dup) { return dup.color;
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'
}); });
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); callback(data);
if (showMenuAbove) { if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove(); $dropdown.data('glDropdown').positionMenuAbove();
} }
}); })
.catch(() => flash(__('Error fetching labels.')));
}, },
renderRow: function(label, instance) { renderRow: function(label, instance) {
var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
......
import axios from './axios_utils';
import Cache from './cache'; import Cache from './cache';
class AjaxCache extends Cache { class AjaxCache extends Cache {
...@@ -18,25 +19,18 @@ class AjaxCache extends Cache { ...@@ -18,25 +19,18 @@ class AjaxCache extends Cache {
let pendingRequest = this.pendingRequests[endpoint]; let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) { if (!pendingRequest) {
pendingRequest = new Promise((resolve, reject) => { pendingRequest = axios.get(endpoint)
// jQuery 2 is not Promises/A+ compatible (missing catch) .then(({ data }) => {
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return this.internalStorage[endpoint] = data;
.then(data => resolve(data), delete this.pendingRequests[endpoint];
(jqXHR, textStatus, errorThrown) => { })
const error = new Error(`${endpoint}: ${errorThrown}`); .catch((e) => {
error.textStatus = textStatus; const error = new Error(`${endpoint}: ${e.message}`);
reject(error); error.textStatus = e.message;
},
); delete this.pendingRequests[endpoint];
}) throw error;
.then((data) => { });
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((error) => {
delete this.pendingRequests[endpoint];
throw error;
});
this.pendingRequests[endpoint] = pendingRequest; this.pendingRequests[endpoint] = pendingRequest;
} }
......
...@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => { ...@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => {
window.activeVueResources -= 1; window.activeVueResources -= 1;
return config; return config;
}, (e) => {
window.activeVueResources -= 1;
return Promise.reject(e);
}); });
export default axios; export default axios;
......
import { getLocationHash } from './url_utility';
import axios from './axios_utils'; import axios from './axios_utils';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
...@@ -28,16 +28,11 @@ export const isInIssuePage = () => { ...@@ -28,16 +28,11 @@ export const isInIssuePage = () => {
return page === 'issues' && action === 'show'; return page === 'issues' && action === 'show';
}; };
export const ajaxGet = url => $.ajax({ export const ajaxGet = url => axios.get(url, {
type: 'GET', params: { format: 'js' },
url, responseType: 'text',
dataType: 'script', }).then(({ data }) => {
}); $.globalEval(data);
export const ajaxPost = (url, data) => $.ajax({
type: 'POST',
url,
data,
}); });
export const rstrip = (val) => { export const rstrip = (val) => {
...@@ -412,7 +407,6 @@ window.gl.utils = { ...@@ -412,7 +407,6 @@ window.gl.utils = {
getGroupSlug, getGroupSlug,
isInIssuePage, isInIssuePage,
ajaxGet, ajaxGet,
ajaxPost,
rstrip, rstrip,
updateTooltipTitle, updateTooltipTitle,
disableButtonIfEmptyField, disableButtonIfEmptyField,
......
/* eslint-disable no-param-reassign, comma-dangle */ /* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
((global) => { ((global) => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
...@@ -10,20 +11,11 @@ ...@@ -10,20 +11,11 @@
} }
fetchConflictsData() { fetchConflictsData() {
return $.ajax({ return axios.get(this.conflictsPath);
dataType: 'json',
url: this.conflictsPath
});
} }
submitResolveConflicts(data) { submitResolveConflicts(data) {
return $.ajax({ return axios.post(this.resolveConflictsPath, data);
url: this.resolveConflictsPath,
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
method: 'POST'
});
} }
} }
......
...@@ -38,24 +38,23 @@ $(() => { ...@@ -38,24 +38,23 @@ $(() => {
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
}, },
created() { created() {
mergeConflictsService mergeConflictsService.fetchConflictsData()
.fetchConflictsData() .then(({ data }) => {
.done((data) => {
if (data.type === 'error') { if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message); mergeConflictsStore.setFailedRequest(data.message);
} else { } else {
mergeConflictsStore.setConflictsData(data); mergeConflictsStore.setConflictsData(data);
} }
})
.error(() => {
mergeConflictsStore.setFailedRequest();
})
.always(() => {
mergeConflictsStore.setLoadingState(false); mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => { this.$nextTick(() => {
syntaxHighlight($('.js-syntax-highlight')); syntaxHighlight($('.js-syntax-highlight'));
}); });
})
.catch(() => {
mergeConflictsStore.setLoadingState(false);
mergeConflictsStore.setFailedRequest();
}); });
}, },
methods: { methods: {
...@@ -82,10 +81,10 @@ $(() => { ...@@ -82,10 +81,10 @@ $(() => {
mergeConflictsService mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData()) .submitResolveConflicts(mergeConflictsStore.getCommitData())
.done((data) => { .then(({ data }) => {
window.location.href = data.redirect_to; window.location.href = data.redirect_to;
}) })
.error(() => { .catch(() => {
mergeConflictsStore.setSubmitState(false); mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!'); new Flash('Failed to save merge conflicts resolutions. Please try again!');
}); });
......
/* eslint-disable no-new, class-methods-use-this */ /* eslint-disable no-new, class-methods-use-this */
import Cookies from 'js-cookie'; 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 BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints'; import bp from './breakpoints';
...@@ -244,15 +245,22 @@ export default class MergeRequestTabs { ...@@ -244,15 +245,22 @@ export default class MergeRequestTabs {
if (this.commitsLoaded) { if (this.commitsLoaded) {
return; return;
} }
this.ajaxGet({
url: `${source}.json`, this.toggleLoading(true);
success: (data) => {
axios.get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html; document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits')); localTimeAgo($('.js-timeago', 'div#commits'));
this.commitsLoaded = true; this.commitsLoaded = true;
this.scrollToElement('#commits'); this.scrollToElement('#commits');
},
}); this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
} }
mountPipelinesView() { mountPipelinesView() {
...@@ -283,9 +291,10 @@ export default class MergeRequestTabs { ...@@ -283,9 +291,10 @@ export default class MergeRequestTabs {
// some pages like MergeRequestsController#new has query parameters on that anchor // some pages like MergeRequestsController#new has query parameters on that anchor
const urlPathname = parseUrlPathname(source); const urlPathname = parseUrlPathname(source);
this.ajaxGet({ this.toggleLoading(true);
url: `${urlPathname}.json${location.search}`,
success: (data) => { axios.get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs'); const $container = $('#diffs');
$container.html(data.html); $container.html(data.html);
...@@ -335,8 +344,13 @@ export default class MergeRequestTabs { ...@@ -335,8 +344,13 @@ export default class MergeRequestTabs {
// (discussion and diff tabs) and `:target` only applies to the first // (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target'); anchor.addClass('target');
} }
},
}); this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
} }
// Show or hide the loading spinner // Show or hide the loading spinner
...@@ -346,17 +360,6 @@ export default class MergeRequestTabs { ...@@ -346,17 +360,6 @@ export default class MergeRequestTabs {
$('.mr-loading-status .loading').toggle(status); $('.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() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); 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 { export default class Milestone {
constructor() { constructor() {
...@@ -33,15 +34,12 @@ export default class Milestone { ...@@ -33,15 +34,12 @@ export default class Milestone {
const tabElId = $target.attr('href'); const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) { if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({ axios.get(endpoint)
url: endpoint, .then(({ data }) => {
dataType: 'JSON', $(tabElId).html(data.html);
}) $target.addClass('is-loaded');
.fail(() => new Flash('Error loading milestone tab')) })
.done((data) => { .catch(() => flash('Error loading milestone tab'));
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
} }
} }
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global Issuable */ /* global Issuable */
/* global ListMilestone */ /* global ListMilestone */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
export default class MilestoneSelect { export default class MilestoneSelect {
...@@ -52,48 +53,47 @@ export default class MilestoneSelect { ...@@ -52,48 +53,47 @@ export default class MilestoneSelect {
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: (term, callback) => $.ajax({ data: (term, callback) => axios.get(milestonesUrl)
url: milestonesUrl .then(({ data }) => {
}).done((data) => { const extraOptions = [];
const extraOptions = []; if (showAny) {
if (showAny) { extraOptions.push({
extraOptions.push({ id: 0,
id: 0, name: '',
name: '', title: 'Any Milestone'
title: 'Any Milestone' });
}); }
} if (showNo) {
if (showNo) { extraOptions.push({
extraOptions.push({ id: -1,
id: -1, name: 'No Milestone',
name: 'No Milestone', title: 'No Milestone'
title: 'No Milestone' });
}); }
} if (showUpcoming) {
if (showUpcoming) { extraOptions.push({
extraOptions.push({ id: -2,
id: -2, name: '#upcoming',
name: '#upcoming', title: 'Upcoming'
title: 'Upcoming' });
}); }
} if (showStarted) {
if (showStarted) { extraOptions.push({
extraOptions.push({ id: -3,
id: -3, name: '#started',
name: '#started', title: 'Started'
title: 'Started' });
}); }
} if (extraOptions.length) {
if (extraOptions.length) { extraOptions.push('divider');
extraOptions.push('divider'); }
}
callback(extraOptions.concat(data)); callback(extraOptions.concat(data));
if (showMenuAbove) { if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove(); $dropdown.data('glDropdown').positionMenuAbove();
} }
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}), }),
renderRow: milestone => ` renderRow: milestone => `
<li data-milestone-id="${milestone.name}"> <li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'> <a href='#' class='dropdown-menu-milestone-link'>
...@@ -200,26 +200,23 @@ export default class MilestoneSelect { ...@@ -200,26 +200,23 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null; data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return axios.put(issueUpdateURL, data)
type: 'PUT', .then(({ data }) => {
url: issueUpdateURL, $dropdown.trigger('loaded.gl.dropdown');
data: data $loading.fadeOut();
}).done((data) => { $selectBox.hide();
$dropdown.trigger('loaded.gl.dropdown'); $value.css('display', '');
$loading.fadeOut(); if (data.milestone != null) {
$selectBox.hide(); data.milestone.full_path = this.currentProject.full_path;
$value.css('display', ''); data.milestone.remaining = timeFor(data.milestone.due_date);
if (data.milestone != null) { data.milestone.name = data.milestone.title;
data.milestone.full_path = this.currentProject.full_path; $value.html(milestoneLinkTemplate(data.milestone));
data.milestone.remaining = timeFor(data.milestone.due_date); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
data.milestone.name = data.milestone.title; } else {
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').text('No');
} else { }
$value.html(milestoneLinkNoneTemplate); });
return $sidebarCollapsedValue.find('span').text('No');
}
});
} }
} }
}); });
......
...@@ -24,7 +24,7 @@ import GLForm from './gl_form'; ...@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import Autosave from './autosave'; import Autosave from './autosave';
import TaskList from './task_list'; 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 imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
...@@ -1399,7 +1399,7 @@ export default class Notes { ...@@ -1399,7 +1399,7 @@ export default class Notes {
* 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
* 3) Build temporary placeholder element (using `createPlaceholderNote`) * 3) Build temporary placeholder element (using `createPlaceholderNote`)
* 4) Show placeholder note on UI * 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 * a) If request is successfully completed
* 1. Remove placeholder element * 1. Remove placeholder element
* 2. Show submitted Note element * 2. Show submitted Note element
...@@ -1481,8 +1481,10 @@ export default class Notes { ...@@ -1481,8 +1481,10 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
// Make request to submit comment on server // Make request to submit comment on server
ajaxPost(formAction, formData) axios.post(formAction, formData)
.then((note) => { .then((res) => {
const note = res.data;
// Submission successful! remove placeholder // Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
...@@ -1555,7 +1557,7 @@ export default class Notes { ...@@ -1555,7 +1557,7 @@ export default class Notes {
} }
$form.trigger('ajax:success', [note]); $form.trigger('ajax:success', [note]);
}).fail(() => { }).catch(() => {
// Submission failed, remove placeholder note and show Flash error message // Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
...@@ -1594,7 +1596,7 @@ export default class Notes { ...@@ -1594,7 +1596,7 @@ export default class Notes {
* *
* 1) Get Form metadata * 1) Get Form metadata
* 2) Update note element with new content * 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 * a) If request is successfully completed
* 1. Show submitted Note element * 1. Show submitted Note element
* b) If request failed * b) If request failed
...@@ -1625,12 +1627,12 @@ export default class Notes { ...@@ -1625,12 +1627,12 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
// Make request to update comment on server // Make request to update comment on server
ajaxPost(formAction, formData) axios.post(formAction, formData)
.then((note) => { .then(({ data }) => {
// Submission successful! render final note element // Submission successful! render final note element
this.updateNote(note, $editingNote); this.updateNote(data, $editingNote);
}) })
.fail(() => { .catch(() => {
// Submission failed, revert back to original note // Submission failed, revert back to original note
$noteBodyText.html(_.escape(cachedNoteBodyText)); $noteBodyText.html(_.escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in'); $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 */ /* 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 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'; import projectSelect from '../../project_select';
export default class Project { export default class Project {
...@@ -79,17 +82,15 @@ export default class Project { ...@@ -79,17 +82,15 @@ export default class Project {
$dropdown = $(this); $dropdown = $(this);
selected = $dropdown.data('selected'); selected = $dropdown.data('selected');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
data: function(term, callback) { data(term, callback) {
return $.ajax({ axios.get($dropdown.data('refs-url'), {
url: $dropdown.data('refs-url'), params: {
data: {
ref: $dropdown.data('ref'), ref: $dropdown.data('ref'),
search: term, search: term,
}, },
dataType: 'json', })
}).done(function(refs) { .then(({ data }) => callback(data))
return callback(refs); .catch(() => flash(__('An error occurred while getting projects')));
});
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
}; };
</script> </script>
<template> <template>
<ul class="nav-links scrolling-tabs"> <ul class="nav-links scrolling-tabs separator">
<li <li
v-for="(tab, i) in tabs" v-for="(tab, i) in tabs"
:key="i" :key="i"
......
...@@ -82,6 +82,10 @@ ...@@ -82,6 +82,10 @@
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
&.mobile-separator {
border-bottom: 1px solid $border-color;
}
} }
} }
...@@ -168,9 +172,9 @@ ...@@ -168,9 +172,9 @@
display: inline-block; display: inline-block;
} }
// Applies on /dashboard/issues
.project-item-select-holder { .project-item-select-holder {
margin: 0; margin: 0;
width: 100%;
} }
&.inline { &.inline {
...@@ -367,7 +371,6 @@ ...@@ -367,7 +371,6 @@
.project-item-select-holder.btn-group { .project-item-select-holder.btn-group {
display: flex; display: flex;
max-width: 350px;
overflow: hidden; overflow: hidden;
float: right; float: right;
......
...@@ -91,6 +91,10 @@ module MergeRequests ...@@ -91,6 +91,10 @@ module MergeRequests
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end 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 end
# Note: Closed merge requests also need approvals reset. # Note: Closed merge requests also need approvals reset.
...@@ -212,7 +216,8 @@ module MergeRequests ...@@ -212,7 +216,8 @@ module MergeRequests
merge_requests.uniq.select(&:source_project) merge_requests.uniq.select(&:source_project)
end 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) @source_merge_requests ||= merge_requests_for(@branch_name)
end end
......
class AttachmentUploader < GitlabUploader class AttachmentUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads prepend ObjectStorage::Extension::RecordsUploads
include UploaderHelper
private private
......
...@@ -8,11 +8,11 @@ class AvatarUploader < GitlabUploader ...@@ -8,11 +8,11 @@ class AvatarUploader < GitlabUploader
model.avatar.file && model.avatar.file.present? model.avatar.file && model.avatar.file.present?
end end
def move_to_store def move_to_cache
false false
end end
def move_to_cache def move_to_store
false false
end end
......
...@@ -15,8 +15,6 @@ class FileUploader < GitlabUploader ...@@ -15,8 +15,6 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)} DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
attr_accessor :model
def self.root def self.root
File.join(options.storage_path, 'uploads') File.join(options.storage_path, 'uploads')
end end
...@@ -62,6 +60,8 @@ class FileUploader < GitlabUploader ...@@ -62,6 +60,8 @@ class FileUploader < GitlabUploader
SecureRandom.hex SecureRandom.hex
end end
attr_accessor :model
def initialize(model, secret = nil) def initialize(model, secret = nil)
@model = model @model = model
@secret = secret @secret = secret
......
...@@ -45,6 +45,10 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -45,6 +45,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
file.present? file.present?
end end
def store_dir
File.join(base_dir, dynamic_segment)
end
def cache_dir def cache_dir
File.join(root, base_dir, 'tmp/cache') File.join(root, base_dir, 'tmp/cache')
end end
...@@ -62,10 +66,6 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -62,10 +66,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
# Designed to be overridden by child uploaders that have a dynamic path # 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 # segment -- that is, a path that changes based on mutable attributes of its
# associated model # 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 def dynamic_segment
raise(NotImplementedError) raise(NotImplementedError)
end end
......
.top-area .top-area
%ul.nav-links %ul.nav-links.mobile-separator
= nav_link(page: dashboard_groups_path) do = nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do = link_to dashboard_groups_path, title: _("Your groups") do
Your groups Your groups
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs .top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .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 = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects Your projects
......
.nav-block .nav-block
%ul.nav-links %ul.nav-links.mobile-separator
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path = link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if current_user.todos.any? - if current_user.todos.any?
.top-area .top-area
%ul.nav-links %ul.nav-links.mobile-separator
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.col-sm-7.brand-holder.pull-left .col-sm-7.brand-holder.pull-left
%h1 %h1
= brand_title = brand_title
= brand_image = brand_image
- if brand_item&.description? - if brand_item&.description?
= brand_text = brand_text
- else - else
......
%ul.nav-links %ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }> %li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do = link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All") = s_("PipelineSchedules|All")
......
- failed_builds = @pipeline.statuses.latest.failed - failed_builds = @pipeline.statuses.latest.failed
.tabs-holder .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 %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 = link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
Pipeline Pipeline
......
%ul.nav-links.event-filter.scrolling-tabs .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all') .fade-left= icon('angle-left')
- if event_filter_visible(:repository) .fade-right= icon('angle-right')
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events') %ul.nav-links.event-filter.scrolling-tabs
- if event_filter_visible(:merge_requests) = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
= event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events') - if event_filter_visible(:repository)
- if event_filter_visible(:issues) = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
= event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events') - if event_filter_visible(:merge_requests)
- if comments_visible? = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
= event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments') - if event_filter_visible(:issues)
= event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team') = 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) }> %li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do = link_to milestones_filter_path(state: 'opened') do
Open Open
......
%ul.nav-links %ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }> %li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do = link_to build_path_proc.call(nil) do
All All
......
- type = local_assigns.fetch(:type, :issues) - type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false) - 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') }> %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 = 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)} #{issuables_state_counter_text(type, :opened)}
......
- subject = local_assigns.fetch(:subject, current_user) - subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false) - 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?) } %li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do = link_to subject_snippets_path(subject) do
All 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 ...@@ -193,6 +193,12 @@ production: &base
# endpoint: 'http://127.0.0.1:9000' # default: nil # endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' # 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 ## GitLab Pages
pages: pages:
enabled: false enabled: false
......
...@@ -346,7 +346,6 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values ...@@ -346,7 +346,6 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values
# Settings.artifact['path'] is deprecated, use `storage_path` instead # Settings.artifact['path'] is deprecated, use `storage_path` instead
Settings.artifacts['path'] = Settings.artifacts['storage_path'] Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings.artifacts['object_store'] ||= Settingslogic.new({}) Settings.artifacts['object_store'] ||= Settingslogic.new({})
Settings.artifacts['object_store']['enabled'] ||= false Settings.artifacts['object_store']['enabled'] ||= false
Settings.artifacts['object_store']['remote_directory'] ||= nil Settings.artifacts['object_store']['remote_directory'] ||= nil
...@@ -413,6 +412,13 @@ Settings.uploads['object_store']['background_upload'] ||= true ...@@ -413,6 +412,13 @@ Settings.uploads['object_store']['background_upload'] ||= true
# Convert upload connection settings to use string keys, to make Fog happy # Convert upload connection settings to use string keys, to make Fog happy
Settings.uploads['object_store']['connection']&.deep_stringify_keys! 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 # 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 @@ ...@@ -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: 20180201145907) do ActiveRecord::Schema.define(version: 20180202111106) 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"
...@@ -2228,7 +2228,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do ...@@ -2228,7 +2228,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do
t.integer "project_id", null: false t.integer "project_id", null: false
end 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| create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate" t.text "certificate"
......
...@@ -139,13 +139,12 @@ module Gitlab ...@@ -139,13 +139,12 @@ module Gitlab
end end
def setup_label def setup_label
return unless @relation_hash['type'] == 'GroupLabel'
# If there's no group, move the label to a project label # 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_hash['project_id'] = nil
@relation_name = :group_label @relation_name = :group_label
else else
@relation_hash['group_id'] = nil
@relation_hash['type'] = 'ProjectLabel' @relation_hash['type'] = 'ProjectLabel'
end end
end end
......
/* global BoardService */ /* global BoardService */
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/labels_select'; import '~/labels_select';
import LabelsSelect from '~/boards/components/labels_select.vue'; import LabelsSelect from '~/boards/components/labels_select.vue';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
...@@ -32,14 +34,16 @@ const label2 = { ...@@ -32,14 +34,16 @@ const label2 = {
}; };
describe('LabelsSelect', () => { describe('LabelsSelect', () => {
let mock;
beforeEach((done) => { beforeEach((done) => {
setFixtures('<div class="test-container"></div>'); setFixtures('<div class="test-container"></div>');
const deferred = new jQuery.Deferred(); mock = new MockAdapter(axios);
spyOn($, 'ajax').and.returnValue(deferred.resolve([ mock.onGet('/some/path').reply(200, [
label, label,
label2, label2,
])); ]);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new IssuableContext(); new IssuableContext();
...@@ -60,6 +64,10 @@ describe('LabelsSelect', () => { ...@@ -60,6 +64,10 @@ describe('LabelsSelect', () => {
Vue.nextTick(done); Vue.nextTick(done);
}); });
afterEach(() => {
mock.restore();
});
describe('canEdit', () => { describe('canEdit', () => {
it('hides Edit button', (done) => { it('hides Edit button', (done) => {
vm.canEdit = false; vm.canEdit = false;
......
/* global BoardService */ /* global BoardService */
import Vue from 'vue'; 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 MilestoneSelect from '~/boards/components/milestone_select.vue';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
import { boardObj } from './mock_data'; import { boardObj } from './mock_data';
...@@ -92,12 +94,18 @@ describe('Milestone select component', () => { ...@@ -92,12 +94,18 @@ describe('Milestone select component', () => {
}); });
describe('clicking dropdown items', () => { describe('clicking dropdown items', () => {
let mock;
beforeEach(() => { beforeEach(() => {
const deferred = new jQuery.Deferred(); mock = new MockAdapater(axios);
spyOn($, 'ajax').and.returnValue(deferred.resolve([ mock.onGet('/test/issue-boards/milestones.json').reply(200, [
milestone, milestone,
milestone2, milestone2,
])); ]);
});
afterEach(() => {
mock.restore();
}); });
it('sets Any Milestone', (done) => { 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 */ /* 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 Issue from '~/issue';
import '~/lib/utils/text_utility'; import '~/lib/utils/text_utility';
...@@ -88,20 +90,24 @@ describe('Issue', function() { ...@@ -88,20 +90,24 @@ describe('Issue', function() {
[true, false].forEach((isIssueInitiallyOpen) => { [true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() { describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
const action = isIssueInitiallyOpen ? 'close' : 'reopen'; const action = isIssueInitiallyOpen ? 'close' : 'reopen';
let mock;
function ajaxSpy(req) { function mockCloseButtonResponseSuccess(url, response) {
if (req.url === this.$triggeredButton.attr('href')) { mock.onPut(url).reply(() => {
expect(req.type).toBe('PUT');
expectNewBranchButtonState(true, false); 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 [200, response];
return null; });
}
function mockCloseButtonResponseError(url) {
mock.onPut(url).networkError();
}
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
});
} }
beforeEach(function() { beforeEach(function() {
...@@ -111,6 +117,11 @@ describe('Issue', function() { ...@@ -111,6 +117,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw'); 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); findElements(isIssueInitiallyOpen);
this.issue = new Issue(); this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen); expectIssueState(isIssueInitiallyOpen);
...@@ -120,71 +131,89 @@ describe('Issue', function() { ...@@ -120,71 +131,89 @@ describe('Issue', function() {
this.$projectIssuesCounter = $('.issue_counter').first(); this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001'); this.$projectIssuesCounter.text('1,001');
this.issueStateDeferred = new jQuery.Deferred(); spyOn(axios, 'get').and.callThrough();
this.canCreateBranchDeferred = new jQuery.Deferred(); });
spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this)); afterEach(() => {
mock.restore();
$('div.flash-alert').remove();
}); });
it(`${action}s the issue`, function() { it(`${action}s the issue`, function(done) {
this.$triggeredButton.trigger('click'); mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
this.issueStateDeferred.resolve({
id: 34 id: 34
}); });
this.canCreateBranchDeferred.resolve({ mockCanCreateBranch(!isIssueInitiallyOpen);
can_create_branch: !isIssueInitiallyOpen
});
expectIssueState(!isIssueInitiallyOpen); this.$triggeredButton.trigger('click');
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); setTimeout(() => {
expectNewBranchButtonState(false, !isIssueInitiallyOpen); 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() { it(`fails to ${action} the issue if saved:false`, function(done) {
this.$triggeredButton.trigger('click'); mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
this.issueStateDeferred.resolve({
saved: false saved: false
}); });
this.canCreateBranchDeferred.resolve({ mockCanCreateBranch(isIssueInitiallyOpen);
can_create_branch: isIssueInitiallyOpen
});
expectIssueState(isIssueInitiallyOpen); this.$triggeredButton.trigger('click');
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage(); setTimeout(() => {
expect(this.$projectIssuesCounter.text()).toBe('1,001'); expectIssueState(isIssueInitiallyOpen);
expectNewBranchButtonState(false, 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.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
this.canCreateBranchDeferred.resolve({
can_create_branch: isIssueInitiallyOpen
});
expectIssueState(isIssueInitiallyOpen); setTimeout(() => {
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); expectIssueState(isIssueInitiallyOpen);
expectErrorMessage(); expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe('1,001'); expectErrorMessage();
expectNewBranchButtonState(false, isIssueInitiallyOpen); expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
done();
});
}); });
it('disables the new branch button if Ajax call fails', function() { 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.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
this.canCreateBranchDeferred.reject();
expectNewBranchButtonState(false, false); 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 = $(); Issue.$btnNewBranch = $();
this.canCreateBranchDeferred = null; this.canCreateBranchDeferred = null;
this.$triggeredButton.trigger('click'); this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
setTimeout(() => {
expect(axios.get).not.toHaveBeenCalled();
done();
});
}); });
}); });
}); });
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
...@@ -6,11 +8,29 @@ import '~/breakpoints'; ...@@ -6,11 +8,29 @@ import '~/breakpoints';
describe('Job', () => { describe('Job', () => {
const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
let mock;
let response;
function waitForPromise() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
preloadFixtures('builds/build-with-artifacts.html.raw'); preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => { beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw'); loadFixtures('builds/build-with-artifacts.html.raw');
spyOn(urlUtils, 'visitUrl');
mock = new MockAdapter(axios);
mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
});
afterEach(() => {
mock.restore();
response = {};
}); });
describe('class constructor', () => { describe('class constructor', () => {
...@@ -55,161 +75,159 @@ describe('Job', () => { ...@@ -55,161 +75,159 @@ describe('Job', () => {
}); });
describe('running build', () => { describe('running build', () => {
it('updates the build trace on an interval', function () { it('updates the build trace on an interval', function (done) {
const deferred1 = $.Deferred(); response = {
const deferred2 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise());
spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update<span>', html: '<span>Update<span>',
status: 'running', status: 'running',
state: 'newstate', state: 'newstate',
append: true, append: true,
complete: false, complete: false,
}); };
deferred2.resolve({
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
complete: true,
});
this.job = new Job(); this.job = new Job();
expect($('#build-trace .js-build-output').text()).toMatch(/Update/); waitForPromise()
expect(this.job.state).toBe('newstate'); .then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001); expect(this.job.state).toBe('newstate');
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); response = {
expect(this.job.state).toBe('finalstate'); html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
complete: true,
};
})
.then(() => jasmine.clock().tick(4001))
.then(waitForPromise)
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(this.job.state).toBe('finalstate');
})
.then(done)
.catch(done.fail);
}); });
it('replaces the entire build trace', () => { it('replaces the entire build trace', (done) => {
const deferred1 = $.Deferred(); response = {
const deferred2 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise());
spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update<span>', html: '<span>Update<span>',
status: 'running', status: 'running',
append: false, append: false,
complete: false, complete: false,
}); };
deferred2.resolve({
html: '<span>Different</span>',
status: 'running',
append: false,
});
this.job = new Job(); this.job = new Job();
expect($('#build-trace .js-build-output').text()).toMatch(/Update/); waitForPromise()
.then(() => {
jasmine.clock().tick(4001); expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); response = {
expect($('#build-trace .js-build-output').text()).toMatch(/Different/); html: '<span>Different</span>',
status: 'running',
append: false,
};
})
.then(() => jasmine.clock().tick(4001))
.then(waitForPromise)
.then(() => {
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
})
.then(done)
.catch(done.fail);
}); });
}); });
describe('truncated information', () => { describe('truncated information', () => {
describe('when size is less than total', () => { describe('when size is less than total', () => {
it('shows information about truncated log', () => { it('shows information about truncated log', (done) => {
spyOn(urlUtils, 'visitUrl'); response = {
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: false, append: false,
size: 50, size: 50,
total: 100, total: 100,
}); };
this.job = new Job(); this.job = new Job();
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); waitForPromise()
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
})
.then(done)
.catch(done.fail);
}); });
it('shows the size in KiB', () => { it('shows the size in KiB', (done) => {
const size = 50; const size = 50;
spyOn(urlUtils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise()); response = {
deferred.resolve({
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: false, append: false,
size, size,
total: 100, total: 100,
}); };
this.job = new Job(); this.job = new Job();
expect( waitForPromise()
document.querySelector('.js-truncated-info-size').textContent.trim(), .then(() => {
).toEqual(`${numberToHumanSize(size)}`); expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${numberToHumanSize(size)}`);
})
.then(done)
.catch(done.fail);
}); });
it('shows incremented size', () => { it('shows incremented size', (done) => {
const deferred1 = $.Deferred(); response = {
const deferred2 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise());
spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: false, append: false,
size: 50, size: 50,
total: 100, total: 100,
}); };
this.job = new Job(); this.job = new Job();
expect( waitForPromise()
document.querySelector('.js-truncated-info-size').textContent.trim(), .then(() => {
).toEqual(`${numberToHumanSize(50)}`); expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
jasmine.clock().tick(4001); ).toEqual(`${numberToHumanSize(50)}`);
deferred2.resolve({ response = {
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: true, append: true,
size: 10, size: 10,
total: 100, total: 100,
}); };
})
expect( .then(() => jasmine.clock().tick(4001))
document.querySelector('.js-truncated-info-size').textContent.trim(), .then(waitForPromise)
).toEqual(`${numberToHumanSize(60)}`); .then(() => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${numberToHumanSize(60)}`);
})
.then(done)
.catch(done.fail);
}); });
it('renders the raw link', () => { it('renders the raw link', () => {
const deferred = $.Deferred(); response = {
spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: false, append: false,
size: 50, size: 50,
total: 100, total: 100,
}); };
this.job = new Job(); this.job = new Job();
...@@ -220,50 +238,50 @@ describe('Job', () => { ...@@ -220,50 +238,50 @@ describe('Job', () => {
}); });
describe('when size is equal than total', () => { describe('when size is equal than total', () => {
it('does not show the trunctated information', () => { it('does not show the trunctated information', (done) => {
const deferred = $.Deferred(); response = {
spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: false, append: false,
size: 100, size: 100,
total: 100, total: 100,
}); };
this.job = new Job(); this.job = new Job();
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); waitForPromise()
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
describe('output trace', () => { describe('output trace', () => {
beforeEach(() => { beforeEach((done) => {
const deferred = $.Deferred(); response = {
spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>', html: '<span>Update</span>',
status: 'success', status: 'success',
append: false, append: false,
size: 50, size: 50,
total: 100, total: 100,
}); };
this.job = new Job(); this.job = new Job();
waitForPromise()
.then(done)
.catch(done.fail);
}); });
it('should render trace controls', () => { it('should render trace controls', () => {
const controllers = document.querySelector('.controllers'); const controllers = document.querySelector('.controllers');
expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined(); expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
expect(controllers.querySelector('.js-erase-link')).toBeDefined(); expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
expect(controllers.querySelector('.js-scroll-up')).toBeDefined(); expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
}); });
it('should render received output', () => { it('should render received output', () => {
...@@ -276,13 +294,13 @@ describe('Job', () => { ...@@ -276,13 +294,13 @@ describe('Job', () => {
describe('getBuildTrace', () => { describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => { it('should request build trace with state parameter', (done) => {
spyOn(jQuery, 'ajax').and.callThrough(); spyOn(axios, 'get').and.callThrough();
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Job(); new Job();
setTimeout(() => { setTimeout(() => {
expect(jQuery.ajax).toHaveBeenCalledWith( expect(axios.get).toHaveBeenCalledWith(
{ url: `${JOB_URL}/trace.json`, data: { state: '' } }, `${JOB_URL}/trace.json`, { params: { state: '' } },
); );
done(); done();
}, 0); }, 0);
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
...@@ -10,35 +12,44 @@ import '~/users_select'; ...@@ -10,35 +12,44 @@ import '~/users_select';
(() => { (() => {
let saveLabelCount = 0; let saveLabelCount = 0;
let mock;
describe('Issue dropdown sidebar', () => { describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html.raw'); preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => { beforeEach(() => {
loadFixtures('static/issue_sidebar_label.html.raw'); loadFixtures('static/issue_sidebar_label.html.raw');
mock = new MockAdapter(axios);
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect(); new LabelsSelect();
spyOn(jQuery, 'ajax').and.callFake((req) => { mock.onGet('/root/test/labels.json').reply(() => {
const d = $.Deferred(); const labels = Array(10).fill().map((_, i) => ({
let LABELS_DATA = []; id: i,
title: `test ${i}`,
color: '#5CB85C',
}));
if (req.url === '/root/test/labels.json') { return [200, labels];
for (let i = 0; i < 10; i += 1) { });
LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
} mock.onPut('/root/test/issues/2.json').reply(() => {
} else if (req.url === '/root/test/issues/2.json') { const labels = Array(saveLabelCount).fill().map((_, i) => ({
const tmp = []; id: i,
for (let i = 0; i < saveLabelCount; i += 1) { title: `test ${i}`,
tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' }); color: '#5CB85C',
} }));
LABELS_DATA = { labels: tmp };
}
d.resolve(LABELS_DATA); return [200, { labels }];
return d.promise();
}); });
}); });
afterEach(() => {
mock.restore();
});
it('changes collapsed tooltip when changing labels when less than 5', (done) => { it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5; saveLabelCount = 5;
$('.edit-link').get(0).click(); $('.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'; import AjaxCache from '~/lib/utils/ajax_cache';
describe('AjaxCache', () => { describe('AjaxCache', () => {
...@@ -87,66 +89,53 @@ describe('AjaxCache', () => { ...@@ -87,66 +89,53 @@ describe('AjaxCache', () => {
}); });
describe('retrieve', () => { describe('retrieve', () => {
let ajaxSpy; let mock;
beforeEach(() => { 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) => { it('stores and returns data from Ajax call if cache is empty', (done) => {
ajaxSpy = (url) => { mock.onGet(dummyEndpoint).reply(200, dummyResponse);
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.resolve(dummyResponse);
return deferred.promise();
};
AjaxCache.retrieve(dummyEndpoint) AjaxCache.retrieve(dummyEndpoint)
.then((data) => { .then((data) => {
expect(data).toBe(dummyResponse); expect(data).toEqual(dummyResponse);
expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse); expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse);
}) })
.then(done) .then(done)
.catch(fail); .catch(fail);
}); });
it('makes no Ajax call if request is pending', () => { it('makes no Ajax call if request is pending', (done) => {
const responseDeferred = $.Deferred(); mock.onGet(dummyEndpoint).reply(200, dummyResponse);
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}`);
AjaxCache.retrieve(dummyEndpoint) AjaxCache.retrieve(dummyEndpoint)
.then(unexpectedResponse) .then(done)
.catch(fail); .catch(fail);
AjaxCache.retrieve(dummyEndpoint) AjaxCache.retrieve(dummyEndpoint)
.then(unexpectedResponse) .then(done)
.catch(fail); .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) => { it('returns undefined if Ajax call fails and cache is empty', (done) => {
const dummyStatusText = 'exploded'; const errorMessage = 'Network Error';
const dummyErrorMessage = 'server exploded'; mock.onGet(dummyEndpoint).networkError();
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.reject(null, dummyStatusText, dummyErrorMessage);
return deferred.promise();
};
AjaxCache.retrieve(dummyEndpoint) AjaxCache.retrieve(dummyEndpoint)
.then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`)) .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`))
.catch((error) => { .catch((error) => {
expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`); expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`);
expect(error.textStatus).toBe(dummyStatusText); expect(error.textStatus).toBe(errorMessage);
done(); done();
}) })
.catch(fail); .catch(fail);
...@@ -154,7 +143,9 @@ describe('AjaxCache', () => { ...@@ -154,7 +143,9 @@ describe('AjaxCache', () => {
it('makes no Ajax call if matching data exists', (done) => { it('makes no Ajax call if matching data exists', (done) => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse; 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) AjaxCache.retrieve(dummyEndpoint)
.then((data) => { .then((data) => {
...@@ -171,12 +162,7 @@ describe('AjaxCache', () => { ...@@ -171,12 +162,7 @@ describe('AjaxCache', () => {
AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse; AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse;
ajaxSpy = (url) => { mock.onGet(dummyEndpoint).reply(200, dummyResponse);
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.resolve(dummyResponse);
return deferred.promise();
};
// Call without forceRetrieve param // Call without forceRetrieve param
AjaxCache.retrieve(dummyEndpoint) AjaxCache.retrieve(dummyEndpoint)
...@@ -189,7 +175,7 @@ describe('AjaxCache', () => { ...@@ -189,7 +175,7 @@ describe('AjaxCache', () => {
// Call with forceRetrieve param // Call with forceRetrieve param
AjaxCache.retrieve(dummyEndpoint, true) AjaxCache.retrieve(dummyEndpoint, true)
.then((data) => { .then((data) => {
expect(data).toBe(dummyResponse); expect(data).toEqual(dummyResponse);
}) })
.then(done) .then(done)
.catch(fail); .catch(fail);
......
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
import * as commonUtils from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
describe('common_utils', () => { describe('common_utils', () => {
...@@ -460,17 +459,6 @@ 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', () => { describe('spriteIcon', () => {
let beforeGon; let beforeGon;
......
/* eslint-disable no-var, comma-dangle, object-shorthand */ /* 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 * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs'; import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle'; import '~/commit/pipelines/pipelines_bundle';
...@@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo'; ...@@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo';
describe('activateTab', function () { describe('activateTab', function () {
beforeEach(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'); loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
this.subject = this.class.activateTab; this.subject = this.class.activateTab;
}); });
...@@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo'; ...@@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo';
describe('setCurrentAction', function () { describe('setCurrentAction', function () {
beforeEach(function () { beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {}); spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
this.subject = this.class.setCurrentAction; this.subject = this.class.setCurrentAction;
}); });
...@@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo'; ...@@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo';
}); });
describe('tabShown', () => { describe('tabShown', () => {
let mock;
beforeEach(function () { beforeEach(function () {
spyOn($, 'ajax').and.callFake(function (options) { mock = new MockAdapter(axios);
options.success({ html: '' }); mock.onGet(/(.*)\/diffs\.json/).reply(200, {
data: { html: '' },
}); });
loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
}); });
afterEach(() => {
mock.restore();
});
describe('with "Side-by-side"/parallel diff view', () => { describe('with "Side-by-side"/parallel diff view', () => {
beforeEach(function () { beforeEach(function () {
this.class.diffViewType = () => 'parallel'; this.class.diffViewType = () => 'parallel';
...@@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo'; ...@@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo';
it('triggers Ajax request to JSON endpoint', function (done) { it('triggers Ajax request to JSON endpoint', function (done) {
const url = '/foo/bar/merge_requests/1/diffs'; 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(); done();
return Promise.resolve({ data: {} });
}); });
this.class.loadDiff(url); this.class.loadDiff(url);
}); });
it('triggers scroll event when diff already loaded', function (done) { 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'); spyOn(document, 'dispatchEvent');
this.class.diffsLoaded = true; this.class.diffsLoaded = true;
...@@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo'; ...@@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo';
describe('with inline diff', () => { describe('with inline diff', () => {
let noteId; let noteId;
let noteLineNumId; let noteLineNumId;
let mock;
beforeEach(() => { beforeEach(() => {
const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture);
...@@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo'; ...@@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo';
.attr('href') .attr('href')
.replace('#', ''); .replace('#', '');
spyOn($, 'ajax').and.callFake(function (options) { mock = new MockAdapter(axios);
options.success(diffsResponse); mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
}); });
afterEach(() => {
mock.restore();
}); });
describe('with note fragment hash', () => { 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); spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0); setTimeout(() => {
expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ expect(noteId.length).toBeGreaterThan(0);
target: jasmine.any(Object), expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
lineType: 'old', target: jasmine.any(Object),
forceShow: true, 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'); spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); 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'; ...@@ -370,6 +395,7 @@ import 'vendor/jquery.scrollTo';
describe('with parallel diff', () => { describe('with parallel diff', () => {
let noteId; let noteId;
let noteLineNumId; let noteLineNumId;
let mock;
beforeEach(() => { beforeEach(() => {
const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture);
...@@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo'; ...@@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo';
.attr('href') .attr('href')
.replace('#', ''); .replace('#', '');
spyOn($, 'ajax').and.callFake(function (options) { mock = new MockAdapter(axios);
options.success(diffsResponse); mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
}); });
afterEach(() => {
mock.restore();
}); });
describe('with note fragment hash', () => { 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); spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0); setTimeout(() => {
expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ expect(noteId.length).toBeGreaterThan(0);
target: jasmine.any(Object), expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
lineType: 'new', target: jasmine.any(Object),
forceShow: true, 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'); spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); setTimeout(() => {
expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
done();
});
}); });
}); });
......
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
import _ from 'underscore'; import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize'; import 'autosize';
import '~/gl_form'; import '~/gl_form';
import '~/lib/utils/text_utility'; import '~/lib/utils/text_utility';
import '~/render_gfm'; import '~/render_gfm';
import Notes from '~/notes'; import Notes from '~/notes';
import timeoutPromise from './helpers/set_timeout_promise_helper';
(function() { (function() {
window.gon || (window.gon = {}); window.gon || (window.gon = {});
...@@ -119,6 +122,7 @@ import Notes from '~/notes'; ...@@ -119,6 +122,7 @@ import Notes from '~/notes';
let noteEntity; let noteEntity;
let $form; let $form;
let $notesContainer; let $notesContainer;
let mock;
beforeEach(() => { beforeEach(() => {
this.notes = new Notes('', []); this.notes = new Notes('', []);
...@@ -136,24 +140,32 @@ import Notes from '~/notes'; ...@@ -136,24 +140,32 @@ import Notes from '~/notes';
$form = $('form.js-main-target-form'); $form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list'); $notesContainer = $('ul.main-notes-list');
$form.find('textarea.js-note-text').val(sampleComment); $form.find('textarea.js-note-text').val(sampleComment);
mock = new MockAdapter(axios);
mock.onPost(/(.*)\/notes$/).reply(200, noteEntity);
}); });
it('updates note and resets edit form', () => { afterEach(() => {
const deferred = $.Deferred(); mock.restore();
spyOn($, 'ajax').and.returnValue(deferred.promise()); });
it('updates note and resets edit form', (done) => {
spyOn(this.notes, 'revertNoteEditForm'); spyOn(this.notes, 'revertNoteEditForm');
spyOn(this.notes, 'setupNewNote'); spyOn(this.notes, 'setupNewNote');
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.resolve(noteEntity);
const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); setTimeout(() => {
const updatedNote = Object.assign({}, noteEntity); const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
updatedNote.note = 'bar'; const updatedNote = Object.assign({}, noteEntity);
this.notes.updateNote(updatedNote, $targetNote); updatedNote.note = 'bar';
this.notes.updateNote(updatedNote, $targetNote);
expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
expect(this.notes.setupNewNote).toHaveBeenCalled();
expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); done();
expect(this.notes.setupNewNote).toHaveBeenCalled(); });
}); });
}); });
...@@ -479,8 +491,19 @@ import Notes from '~/notes'; ...@@ -479,8 +491,19 @@ import Notes from '~/notes';
}; };
let $form; let $form;
let $notesContainer; let $notesContainer;
let mock;
function mockNotesPost() {
mock.onPost(/(.*)\/notes$/).reply(200, note);
}
function mockNotesPostError() {
mock.onPost(/(.*)\/notes$/).networkError();
}
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
this.notes = new Notes('', []); this.notes = new Notes('', []);
window.gon.current_username = 'root'; window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator'; window.gon.current_user_fullname = 'Administrator';
...@@ -489,63 +512,92 @@ import Notes from '~/notes'; ...@@ -489,63 +512,92 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').val(sampleComment); $form.find('textarea.js-note-text').val(sampleComment);
}); });
afterEach(() => {
mock.restore();
});
it('should show placeholder note while new comment is being posted', () => { it('should show placeholder note while new comment is being posted', () => {
mockNotesPost();
$('.js-comment-button').click(); $('.js-comment-button').click();
expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true); expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
}); });
it('should remove placeholder note when new comment is done posting', () => { it('should remove placeholder note when new comment is done posting', (done) => {
const deferred = $.Deferred(); mockNotesPost();
spyOn($, 'ajax').and.returnValue(deferred.promise());
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.resolve(note); setTimeout(() => {
expect($notesContainer.find('.note.being-posted').length).toEqual(0); expect($notesContainer.find('.note.being-posted').length).toEqual(0);
done();
});
}); });
it('should show actual note element when new comment is done posting', () => { it('should show actual note element when new comment is done posting', (done) => {
const deferred = $.Deferred(); mockNotesPost();
spyOn($, 'ajax').and.returnValue(deferred.promise());
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.resolve(note); setTimeout(() => {
expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
done();
});
}); });
it('should reset Form when new comment is done posting', () => { it('should reset Form when new comment is done posting', (done) => {
const deferred = $.Deferred(); mockNotesPost();
spyOn($, 'ajax').and.returnValue(deferred.promise());
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.resolve(note); setTimeout(() => {
expect($form.find('textarea.js-note-text').val()).toEqual(''); expect($form.find('textarea.js-note-text').val()).toEqual('');
done();
});
}); });
it('should show flash error message when new comment failed to be posted', () => { it('should show flash error message when new comment failed to be posted', (done) => {
const deferred = $.Deferred(); mockNotesPostError();
spyOn($, 'ajax').and.returnValue(deferred.promise());
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.reject(); setTimeout(() => {
expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true); expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
done();
});
}); });
it('should show flash error message when comment failed to be updated', () => { it('should show flash error message when comment failed to be updated', (done) => {
const deferred = $.Deferred(); mockNotesPost();
spyOn($, 'ajax').and.returnValue(deferred.promise());
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.resolve(note); timeoutPromise()
const $noteEl = $notesContainer.find(`#note_${note.id}`); .then(() => {
$noteEl.find('.js-note-edit').click(); const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('textarea.js-note-text').val(updatedComment); $noteEl.find('.js-note-edit').click();
$noteEl.find('.js-comment-save-button').click(); $noteEl.find('textarea.js-note-text').val(updatedComment);
deferred.reject(); mock.restore();
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals mockNotesPostError();
expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown $noteEl.find('.js-comment-save-button').click();
})
.then(timeoutPromise)
.then(() => {
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
done();
})
.catch(done.fail);
}); });
}); });
...@@ -563,8 +615,12 @@ import Notes from '~/notes'; ...@@ -563,8 +615,12 @@ import Notes from '~/notes';
}; };
let $form; let $form;
let $notesContainer; let $notesContainer;
let mock;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
mock.onPost(/(.*)\/notes$/).reply(200, note);
this.notes = new Notes('', []); this.notes = new Notes('', []);
window.gon.current_username = 'root'; window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator'; window.gon.current_user_fullname = 'Administrator';
...@@ -582,15 +638,20 @@ import Notes from '~/notes'; ...@@ -582,15 +638,20 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').val(sampleComment); $form.find('textarea.js-note-text').val(sampleComment);
}); });
it('should remove slash command placeholder when comment with slash commands is done posting', () => { afterEach(() => {
const deferred = $.Deferred(); mock.restore();
spyOn($, 'ajax').and.returnValue(deferred.promise()); });
it('should remove slash command placeholder when comment with slash commands is done posting', (done) => {
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click(); $('.js-comment-button').click();
expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
deferred.resolve(note);
expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed setTimeout(() => {
expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
done();
});
}); });
}); });
...@@ -607,8 +668,12 @@ import Notes from '~/notes'; ...@@ -607,8 +668,12 @@ import Notes from '~/notes';
}; };
let $form; let $form;
let $notesContainer; let $notesContainer;
let mock;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
mock.onPost(/(.*)\/notes$/).reply(200, note);
this.notes = new Notes('', []); this.notes = new Notes('', []);
window.gon.current_username = 'root'; window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator'; window.gon.current_user_fullname = 'Administrator';
...@@ -617,19 +682,24 @@ import Notes from '~/notes'; ...@@ -617,19 +682,24 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').html(sampleComment); $form.find('textarea.js-note-text').html(sampleComment);
}); });
it('should not render a script tag', () => { afterEach(() => {
const deferred = $.Deferred(); mock.restore();
spyOn($, 'ajax').and.returnValue(deferred.promise()); });
it('should not render a script tag', (done) => {
$('.js-comment-button').click(); $('.js-comment-button').click();
deferred.resolve(note); setTimeout(() => {
const $noteEl = $notesContainer.find(`#note_${note.id}`); const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click(); $noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').html(updatedComment); $noteEl.find('textarea.js-note-text').html(updatedComment);
$noteEl.find('.js-comment-save-button').click(); $noteEl.find('.js-comment-save-button').click();
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); done();
expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); });
}); });
}); });
......
...@@ -236,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -236,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
labels = project.issues.first.labels labels = project.issues.first.labels
expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0)) 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
end end
shared_examples 'restores group correctly' do |**results| shared_examples 'restores group correctly' do |**results|
it 'has group label' do it 'has group label' do
expect(project.group.labels.size).to eq(results.fetch(:labels, 0)) 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 end
it 'has group milestone' do 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 ...@@ -80,6 +80,14 @@ describe MergeRequests::RefreshService do
expect(@fork_merge_request.approvals).not_to be_empty expect(@fork_merge_request.approvals).not_to be_empty
end 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 context 'when source branch ref does not exists' do
before do before do
DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
...@@ -486,37 +494,21 @@ describe MergeRequests::RefreshService do ...@@ -486,37 +494,21 @@ describe MergeRequests::RefreshService do
end end
it 'references the commit that caused the Work in Progress status' do it 'references the commit that caused the Work in Progress status' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') wip_merge_request = create(:merge_request,
allow(refresh_service).to receive(:find_new_commits) source_project: @project,
refresh_service.instance_variable_set("@commits", [ source_branch: 'wip',
double( target_branch: 'master',
id: 'aaaaaaa', target_project: @project)
sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e',
short_id: 'aaaaaaa', commits = wip_merge_request.commits
title: 'Fix issue', oldrev = commits.last.id
work_in_progress?: false newrev = commits.first.id
), wip_commit = wip_merge_request.commits.find(&:work_in_progress?)
double(
id: 'bbbbbbb', refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
sha: '498214de67004b1da3d820901307bed2a68a8ef6',
short_id: 'bbbbbbb', expect(wip_merge_request.reload.notes.last.note).to eq(
title: 'fixup! Fix issue', "marked as a **Work In Progress** from #{wip_commit.id}"
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"
) )
end 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' require 'spec_helper'
describe AvatarUploader do describe AvatarUploader do
let(:model) { build_stubbed(:user) } let(:model) { create(:user, :with_avatar) }
let(:uploader) { described_class.new(model, :avatar) } let(:uploader) { described_class.new(model, :avatar) }
let(:upload) { create(:upload, model: model) } 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