Commit 76f90950 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-01-19' into 'master'

CE upstream - Friday

Closes #1366, gitlab-ce#42157, and gitlab-ce#41989

See merge request gitlab-org/gitlab-ee!4161
parents 1f3e8f4f 44df7165
This diff is collapsed.
...@@ -353,10 +353,10 @@ group :development, :test do ...@@ -353,10 +353,10 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'gitlab-styles', '~> 2.2.0', require: false gem 'gitlab-styles', '~> 2.3', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines # Pin these dependencies, otherwise a new rule could break the CI pipelines
gem 'rubocop', '~> 0.52.0' gem 'rubocop', '~> 0.52.1'
gem 'rubocop-rspec', '~> 1.20.1' gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
......
...@@ -329,7 +329,7 @@ GEM ...@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-license (1.0.0) gitlab-license (1.0.0)
gitlab-markup (1.6.3) gitlab-markup (1.6.3)
gitlab-styles (2.2.0) gitlab-styles (2.3.0)
rubocop (~> 0.51) rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19) rubocop-rspec (~> 1.19)
...@@ -612,7 +612,7 @@ GEM ...@@ -612,7 +612,7 @@ GEM
rubypants (~> 0.2) rubypants (~> 0.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.0) parallel (1.12.1)
parser (2.4.0.2) parser (2.4.0.2)
ast (~> 2.3) ast (~> 2.3)
parslet (1.5.0) parslet (1.5.0)
...@@ -815,7 +815,7 @@ GEM ...@@ -815,7 +815,7 @@ GEM
pg pg
rails rails
sqlite3 sqlite3
rubocop (0.52.0) rubocop (0.52.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.4.0.2, < 3.0) parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
...@@ -824,8 +824,8 @@ GEM ...@@ -824,8 +824,8 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1) rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51) rubocop (>= 0.51)
rubocop-rspec (1.20.1) rubocop-rspec (1.22.1)
rubocop (>= 0.51.0) rubocop (>= 0.52.1)
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-prof (0.16.2) ruby-prof (0.16.2)
...@@ -1096,7 +1096,7 @@ DEPENDENCIES ...@@ -1096,7 +1096,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.2.0) gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
...@@ -1200,8 +1200,8 @@ DEPENDENCIES ...@@ -1200,8 +1200,8 @@ DEPENDENCIES
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.52.0) rubocop (~> 0.52.1)
rubocop-rspec (~> 1.20.1) rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
......
...@@ -245,6 +245,7 @@ const Api = { ...@@ -245,6 +245,7 @@ const Api = {
(jqXHR, textStatus, errorThrown) => { (jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`); const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus; error.textStatus = textStatus;
if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error); reject(error);
}, },
); );
......
...@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { ...@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => { flashEl.addEventListener('transitionend', () => {
flashEl.remove(); flashEl.remove();
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, { }, {
once: true, once: true,
passive: true, passive: true,
...@@ -64,6 +65,7 @@ const createFlash = function createFlash( ...@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document, parent = document,
actionConfig = null, actionConfig = null,
fadeTransition = true, fadeTransition = true,
addBodyClass = false,
) { ) {
const flashContainer = parent.querySelector('.flash-container'); const flashContainer = parent.querySelector('.flash-container');
...@@ -86,6 +88,8 @@ const createFlash = function createFlash( ...@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block'; flashContainer.style.display = 'block';
if (addBodyClass) document.body.classList.add('flash-shown');
return flashContainer; return flashContainer;
}; };
......
...@@ -68,12 +68,8 @@ export default { ...@@ -68,12 +68,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR }) this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.$store.dispatch('getTreeData', { this.commitMessage = '';
projectId: this.currentProjectId, this.startNewMR = false;
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
...@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)" ...@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)"
type="submit" type="submit"
:disabled="commitButtonDisabled" :disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10" class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
> >
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
......
...@@ -70,7 +70,10 @@ export default { ...@@ -70,7 +70,10 @@ export default {
this.editor.createInstance(this.$refs.editor); this.editor.createInstance(this.$refs.editor);
}) })
.then(() => this.setupEditor()) .then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.')); .catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
}, },
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile) return;
......
...@@ -37,9 +37,12 @@ ...@@ -37,9 +37,12 @@
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
levelIndentation() { levelIndentation() {
if (this.file.level > 0) {
return { return {
marginLeft: `${this.file.level * 16}px`, marginLeft: `${this.file.level * 16}px`,
}; };
}
return {};
}, },
shortId() { shortId() {
return this.file.id.substr(0, 8); return this.file.id.substr(0, 8);
...@@ -114,7 +117,7 @@ ...@@ -114,7 +117,7 @@
/> />
<i <i
class="fa" class="fa"
v-if="changedClass" v-if="file.changed || file.tempFile"
:class="changedClass" :class="changedClass"
aria-hidden="true" aria-hidden="true"
> >
......
...@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => { ...@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
} }
}) })
.catch((e) => { .catch((e) => {
flash('Error while loading the branch files. Please try again.'); flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e; throw e;
}); });
} }
}) })
.catch((e) => { .catch((e) => {
flash('Error while loading the project data. Please try again.'); flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e; throw e;
}); });
} }
......
...@@ -55,7 +55,7 @@ export default class Editor { ...@@ -55,7 +55,7 @@ export default class Editor {
attachModel(model) { attachModel(model) {
this.instance.setModel(model.getModel()); this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model; this.currentModel = model;
...@@ -68,7 +68,7 @@ export default class Editor { ...@@ -68,7 +68,7 @@ export default class Editor {
return acc; return acc;
}, {})); }, {}));
this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
clearEditor() { clearEditor() {
......
...@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility'; ...@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash'; import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
...@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) => ...@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
return false; return false;
}) })
.catch(() => flash('Error checking branch data. Please try again.')); .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = ( export const commitChanges = (
{ commit, state, dispatch, getters }, { commit, state, dispatch, getters },
...@@ -92,7 +93,7 @@ export const commitChanges = ( ...@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => { .then((data) => {
const { branch } = payload; const { branch } = payload;
if (!data.short_id) { if (!data.short_id) {
flash(data.message); flash(data.message, 'alert', document, null, false, true);
return; return;
} }
...@@ -105,19 +106,25 @@ export const commitChanges = ( ...@@ -105,19 +106,25 @@ export const commitChanges = (
}, },
}; };
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
flash( flash(
`Your changes have been committed. Commit ${data.short_id} with ${ commitMsg,
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
'notice', 'notice',
); document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) { if (newMr) {
dispatch('discardAllChanges');
dispatch( dispatch(
'redirectToUrl', 'redirectToUrl',
`${ `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
); );
} else { } else {
commit(types.SET_BRANCH_WORKING_REFERENCE, { commit(types.SET_BRANCH_WORKING_REFERENCE, {
...@@ -134,12 +141,18 @@ export const commitChanges = ( ...@@ -134,12 +141,18 @@ export const commitChanges = (
}); });
dispatch('discardAllChanges'); dispatch('discardAllChanges');
dispatch('closeAllFiles');
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
}) })
.catch(() => flash('Error committing changes. Please try again.')); .catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.responseJSON && err.responseJSON.message) {
errMsg += ` (${stripHtml(err.responseJSON.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = ( export const createTempEntry = (
{ state, dispatch }, { state, dispatch },
......
...@@ -17,7 +17,7 @@ export const getBranchData = ( ...@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading branch data. Please try again.'); flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
}); });
} else { } else {
......
...@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => { ...@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.'); flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
}); });
}; };
...@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile ...@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
.then((raw) => { .then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); commit(types.SET_FILE_RAW_DATA, { file, raw });
}) })
.catch(() => flash('Error loading file content. Please try again.')); .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ commit }, { file, content }) => { export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content }); commit(types.UPDATE_FILE_CONTENT, { file, content });
}; };
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => { export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
}; };
export const setFileEOL = ({ state, commit }, { eol }) => { export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
}; };
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
}
}; };
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
...@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI ...@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
url: newUrl, url: newUrl,
}); });
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, { commit(types.CREATE_TMP_FILE, {
parent, parent,
......
...@@ -18,7 +18,7 @@ export const getProjectData = ( ...@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading project data. Please try again.'); flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); reject(new Error(`Project not loaded ${namespace}/${projectId}`));
}); });
} else { } else {
......
...@@ -52,7 +52,7 @@ export const getTreeData = ( ...@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data); resolve(data);
}) })
.catch((e) => { .catch((e) => {
flash('Error loading tree data. Please try again.'); flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree); if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e); reject(e);
}); });
...@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree); dispatch('getLastCommitData', tree);
}) })
.catch(() => flash('Error fetching log data.')); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const updateDirectoryData = ( export const updateDirectoryData = (
......
...@@ -64,7 +64,7 @@ export default { ...@@ -64,7 +64,7 @@ export default {
}, },
[types.DISCARD_FILE_CHANGES](state, file) { [types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, { Object.assign(file, {
content: '', content: file.raw,
changed: false, changed: false,
}); });
}, },
......
...@@ -152,6 +152,13 @@ ...@@ -152,6 +152,13 @@
hasUpdated() { hasUpdated() {
return !!this.state.updatedAt; return !!this.state.updatedAt;
}, },
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
}, },
created() { created() {
this.service = new Service(this.endpoint); this.service = new Service(this.endpoint);
...@@ -176,6 +183,8 @@ ...@@ -176,6 +183,8 @@
} }
}); });
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable); eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable); eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm); eventHub.$on('close.form', this.closeForm);
...@@ -186,8 +195,17 @@ ...@@ -186,8 +195,17 @@
eventHub.$off('update.issuable', this.updateIssuable); eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm); eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm); eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
}, },
methods: { methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
this.showForm = true; this.showForm = true;
......
...@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) { ...@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace * @param {*} replace
* @returns {String} * @returns {String}
*/ */
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
...@@ -66,9 +66,7 @@ ...@@ -66,9 +66,7 @@
<template> <template>
<div class="note-header-info"> <div class="note-header-info">
<a :href="author.path"> <a :href="author.path">
<span class="note-header-author-name"> <span class="note-header-author-name">{{ author.name }}</span>
{{ author.name }}
</span>
<span class="note-headline-light"> <span class="note-headline-light">
@{{ author.username }} @{{ author.username }}
</span> </span>
......
import Compare from '~/compare';
import MergeRequest from '~/merge_request';
export default () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
new Compare({ // eslint-disable-line no-new
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
}
};
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
<template> <template>
<component <component
:is="rootElementType" :is="rootElementType"
class="text-center"> class="loading-container text-center">
<i <i
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
:class="cssClass" :class="cssClass"
......
...@@ -174,12 +174,13 @@ ...@@ -174,12 +174,13 @@
&.user-authored { &.user-authored {
cursor: default; cursor: default;
opacity: 0.65; background-color: $gray-light;
border-color: $theme-gray-200;
color: $gl-text-color-disabled;
&:hover, gl-emoji {
&:active { opacity: 0.4;
background-color: $white-light; filter: grayscale(100%);
border-color: $border-color;
} }
} }
......
...@@ -224,14 +224,6 @@ ...@@ -224,14 +224,6 @@
@include btn-with-margin; @include btn-with-margin;
} }
&.disabled {
pointer-events: auto !important;
}
&[disabled] {
pointer-events: none !important;
}
.fa-caret-down, .fa-caret-down,
.fa-chevron-down { .fa-chevron-down {
margin-left: 5px; margin-left: 5px;
...@@ -454,3 +446,28 @@ ...@@ -454,3 +446,28 @@
.btn-svg svg { .btn-svg svg {
@include btn-svg; @include btn-svg;
} }
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light !important;
border-color: $theme-gray-200 !important;
color: $gl-text-color-disabled !important;
opacity: 1 !important;
cursor: default !important;
i {
color: $gl-text-color-disabled !important;
}
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn,
.dropdown-toggle[disabled],
[disabled].dropdown-menu-toggle {
@extend %disabled;
&:hover {
@extend %disabled;
}
}
...@@ -63,11 +63,6 @@ ...@@ -63,11 +63,6 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&[disabled] {
opacity: .65;
cursor: not-allowed;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -165,6 +165,7 @@ $gl-text-color-tertiary: #949494; ...@@ -165,6 +165,7 @@ $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6; $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600; $gl-text-green: $green-600;
$gl-text-green-hover: $green-700; $gl-text-green-hover: $green-700;
$gl-text-red: $red-500; $gl-text-red: $red-500;
...@@ -260,6 +261,8 @@ $general-hover-transition-duration: 100ms; ...@@ -260,6 +261,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear; $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232); $highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px; $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$issue-box-upcoming-bg: #8f8f8f; $issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54; $pages-group-name-color: #4c4e54;
......
...@@ -391,11 +391,17 @@ ...@@ -391,11 +391,17 @@
.dropdown-toggle { .dropdown-toggle {
float: right; float: right;
.toggle-icon { i {
color: $white-light; color: $white-light;
padding-right: 2px; padding-right: 2px;
margin-top: 2px; margin-top: 2px;
} }
&[disabled] {
i {
color: $gl-text-color-disabled;
}
}
} }
.dropdown-menu { .dropdown-menu {
......
...@@ -112,6 +112,11 @@ table.table tr td.multi-file-table-name { ...@@ -112,6 +112,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle; vertical-align: middle;
margin-right: 2px; margin-right: 2px;
} }
.loading-container {
margin-right: 4px;
display: inline-block;
}
} }
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
...@@ -252,7 +257,6 @@ table.table tr td.multi-file-table-name { ...@@ -252,7 +257,6 @@ table.table tr td.multi-file-table-name {
display: flex; display: flex;
position: relative; position: relative;
flex-direction: column; flex-direction: column;
height: 100%;
width: 290px; width: 290px;
padding: 0; padding: 0;
background-color: $gray-light; background-color: $gray-light;
...@@ -261,6 +265,11 @@ table.table tr td.multi-file-table-name { ...@@ -261,6 +265,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar { .projects-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
} }
.multi-file-commit-panel-inner { .multi-file-commit-panel-inner {
...@@ -508,19 +517,70 @@ table.table tr td.multi-file-table-name { ...@@ -508,19 +517,70 @@ table.table tr td.multi-file-table-name {
} }
} }
.ide-flash-container.flash-container { .ide.nav-only {
.flash-container {
margin-top: $header-height; margin-top: $header-height;
margin-bottom: 0; margin-bottom: 0;
}
.alert-wrapper .flash-container .flash-alert:last-child,
.alert-wrapper .flash-container .flash-notice:last-child {
margin-bottom: 0;
}
.content {
margin-top: $header-height;
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
}
}
} }
.with-performance-bar { .with-performance-bar .ide.nav-only {
.ide-flash-container.flash-container { .flash-container {
margin-top: $header-height + $performance-bar-height; margin-top: #{$header-height + $performance-bar-height};
}
.content {
margin-top: #{$header-height + $performance-bar-height};
} }
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height}); height: calc(100vh - #{$header-height + $performance-bar-height});
} }
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
}
} }
......
require 'webpack/rails/manifest' require 'webpack/rails/manifest'
module WebpackHelper module WebpackHelper
def webpack_bundle_tag(bundle) def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle)) javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end end
# override webpack-rails gem helper until changes can make it upstream # override webpack-rails gem helper until changes can make it upstream
def gitlab_webpack_asset_paths(source, extension: nil) def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present? return "" unless source.present?
paths = Webpack::Rails::Manifest.asset_paths(source) paths = Webpack::Rails::Manifest.asset_paths(source)
...@@ -14,10 +14,12 @@ module WebpackHelper ...@@ -14,10 +14,12 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" } paths.select! { |p| p.ends_with? ".#{extension}" }
end end
unless force_same_domain
force_host = webpack_public_host force_host = webpack_public_host
if force_host if force_host
paths.map! { |p| "#{force_host}#{p}" } paths.map! { |p| "#{force_host}#{p}" }
end end
end
paths paths
end end
......
...@@ -318,6 +318,7 @@ class Project < ActiveRecord::Base ...@@ -318,6 +318,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
......
...@@ -41,7 +41,7 @@ class ProjectStatistics < ActiveRecord::Base ...@@ -41,7 +41,7 @@ class ProjectStatistics < ActiveRecord::Base
def update_build_artifacts_size def update_build_artifacts_size
self.build_artifacts_size = self.build_artifacts_size =
project.builds.sum(:artifacts_size) + project.builds.sum(:artifacts_size) +
Ci::JobArtifact.artifacts_size_for(self) Ci::JobArtifact.artifacts_size_for(self.project)
end end
def update_storage_size def update_storage_size
......
class Route < ActiveRecord::Base class Route < ActiveRecord::Base
include CaseSensitivity
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true validates :source, presence: true
...@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base ...@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base
validate :ensure_permanent_paths, if: :path_changed? validate :ensure_permanent_paths, if: :path_changed?
before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed? after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path after_update :create_redirect_for_old_path
...@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base ...@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base
def conflicting_redirect_exists? def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists? RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end end
def delete_conflicting_orphaned_routes
conflicting = self.class.iwhere(path: path)
conflicting_orphaned_routes = conflicting.select do |route|
route.source.nil?
end
conflicting_orphaned_routes.each(&:destroy)
end
end end
- @body_class = 'ide'
- page_title 'IDE' - page_title 'IDE'
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide' = webpack_bundle_tag 'ide', force_same_domain: true
.ide-flash-container.flash-container
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} } #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center .text-center
......
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } } %body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } }
= render 'peek/bar' = render 'peek/bar'
= render "layouts/header/default" = render "layouts/header/default"
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
= render "layouts/broadcast" = render "layouts/broadcast"
= yield :flash_message = yield :flash_message
= render "layouts/flash" = render "layouts/flash"
.content{ id: "content-body" }
= yield = yield
---
title: Set standard disabled state for all buttons
merge_request:
author:
type: other
---
title: Fix the Projects API with_issues_enabled filter behaving incorrectly
any user
merge_request: 12724
author: Jan Christophersen
type: fixed
---
title: Fix a bug calculating artifact size for project statistics
merge_request: 16539
author:
type: fixed
---
title: Correctly escape UTF-8 path elements for uploads
merge_request: 16560
author:
type: fixed
---
title: Ensure that users can reclaim a namespace or project path that is blocked by
an orphaned route
merge_request: 16242
author:
type: fixed
...@@ -133,7 +133,12 @@ var config = { ...@@ -133,7 +133,12 @@ var config = {
{ {
test: /\_worker\.js$/, test: /\_worker\.js$/,
use: [ use: [
{ loader: 'worker-loader' }, {
loader: 'worker-loader',
options: {
inline: true
}
},
{ loader: 'babel-loader' }, { loader: 'babel-loader' },
], ],
}, },
......
...@@ -49,7 +49,7 @@ Add the following to your `sshd_config` file. This is usuaully located at ...@@ -49,7 +49,7 @@ Add the following to your `sshd_config` file. This is usuaully located at
Omnibus Docker: Omnibus Docker:
``` ```
AuthorizedKeysCommand /opt/embedded/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
AuthorizedKeysCommandUser git AuthorizedKeysCommandUser git
``` ```
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stripeHtml } from '~/lib/utils/text_utility'; import { stripHtml } from '~/lib/utils/text_utility';
export default class MergeRequestStore extends CEMergeRequestStore { export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) { constructor(data) {
...@@ -103,7 +103,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -103,7 +103,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
.filter(item => unapproved.find(el => el === item.vulnerability)) || []; .filter(item => unapproved.find(el => el === item.vulnerability)) || [];
} }
/** /**
* Dast Report sends some keys in HTML, we need to stripe the `<p>` tags. * Dast Report sends some keys in HTML, we need to strip the `<p>` tags.
* This should be moved to the backend. * This should be moved to the backend.
* *
* @param {Array} data * @param {Array} data
...@@ -112,7 +112,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -112,7 +112,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
setDastReport(data) { setDastReport(data) {
this.dastReport = data.site.alerts.map(alert => ({ this.dastReport = data.site.alerts.map(alert => ({
name: alert.name, name: alert.name,
parsedDescription: stripeHtml(alert.desc, ' '), parsedDescription: stripHtml(alert.desc, ' '),
priority: alert.riskdesc, priority: alert.riskdesc,
...alert, ...alert,
})); }));
......
...@@ -82,9 +82,9 @@ module API ...@@ -82,9 +82,9 @@ module API
def present_projects(projects, options = {}) def present_projects(projects, options = {})
projects = reorder_projects(projects) projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics] projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled] projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = paginate(projects) projects = paginate(projects)
if current_user if current_user
......
...@@ -50,7 +50,7 @@ module Banzai ...@@ -50,7 +50,7 @@ module Banzai
end end
def process_link_to_upload_attr(html_attr) def process_link_to_upload_attr(html_attr)
path_parts = [html_attr.value] path_parts = [Addressable::URI.unescape(html_attr.value)]
if group if group
path_parts.unshift(relative_url_root, 'groups', group.full_path, '-') path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
...@@ -58,13 +58,13 @@ module Banzai ...@@ -58,13 +58,13 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path) path_parts.unshift(relative_url_root, project.full_path)
end end
path = File.join(*path_parts) path = Addressable::URI.escape(File.join(*path_parts))
html_attr.value = html_attr.value =
if context[:only_path] if context[:only_path]
path path
else else
URI.join(Gitlab.config.gitlab.base_url, path).to_s Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
end end
end end
......
...@@ -33,9 +33,9 @@ module Gitlab ...@@ -33,9 +33,9 @@ module Gitlab
object object
end end
def initialize(repository, name, target, derefenced_target) def initialize(repository, name, target, dereferenced_target)
@name = Gitlab::Git.ref_name(name) @name = Gitlab::Git.ref_name(name)
@dereferenced_target = derefenced_target @dereferenced_target = dereferenced_target
@target = if target.respond_to?(:oid) @target = if target.respond_to?(:oid)
target.oid target.oid
elsif target.respond_to?(:name) elsif target.respond_to?(:name)
......
...@@ -183,11 +183,15 @@ describe('Flash', () => { ...@@ -183,11 +183,15 @@ describe('Flash', () => {
}); });
it('adds flash element into container', () => { it('adds flash element into container', () => {
flash('test'); flash('test', 'alert', document, null, false, true);
expect( expect(
document.querySelector('.flash-alert'), document.querySelector('.flash-alert'),
).not.toBeNull(); ).not.toBeNull();
expect(
document.body.className,
).toContain('flash-shown');
}); });
it('adds flash into specified parent', () => { it('adds flash into specified parent', () => {
...@@ -220,13 +224,17 @@ describe('Flash', () => { ...@@ -220,13 +224,17 @@ describe('Flash', () => {
}); });
it('removes element after clicking', () => { it('removes element after clicking', () => {
flash('test', 'alert', document, null, false); flash('test', 'alert', document, null, false, true);
document.querySelector('.flash-alert').click(); document.querySelector('.flash-alert').click();
expect( expect(
document.querySelector('.flash-alert'), document.querySelector('.flash-alert'),
).toBeNull(); ).toBeNull();
expect(
document.body.className,
).not.toContain('flash-shown');
}); });
describe('with actionConfig', () => { describe('with actionConfig', () => {
......
...@@ -218,6 +218,39 @@ describe('Issuable output', () => { ...@@ -218,6 +218,39 @@ describe('Issuable output', () => {
}); });
}); });
describe('shows dialog when issue has unsaved changed', () => {
it('confirms on title change', (done) => {
vm.showForm = true;
vm.state.titleText = 'title has changed';
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
Vue.nextTick(() => {
expect(e.returnValue).not.toBeNull();
done();
});
});
it('confirms on description change', (done) => {
vm.showForm = true;
vm.state.descriptionText = 'description has changed';
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
Vue.nextTick(() => {
expect(e.returnValue).not.toBeNull();
done();
});
});
it('does nothing when nothing has changed', (done) => {
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
Vue.nextTick(() => {
expect(e.returnValue).toBeNull();
done();
});
});
});
describe('error when updating', () => { describe('error when updating', () => {
beforeEach(() => { beforeEach(() => {
spyOn(window, 'Flash').and.callThrough(); spyOn(window, 'Flash').and.callThrough();
......
...@@ -61,13 +61,13 @@ describe('text_utility', () => { ...@@ -61,13 +61,13 @@ describe('text_utility', () => {
}); });
}); });
describe('stripeHtml', () => { describe('stripHtml', () => {
it('replaces html tag with the default replacement', () => { it('replaces html tag with the default replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
}); });
it('replaces html tags with the provided replacement', () => { it('replaces html tags with the provided replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
}); });
}); });
}); });
...@@ -300,19 +300,6 @@ describe('Multi-file store actions', () => { ...@@ -300,19 +300,6 @@ describe('Multi-file store actions', () => {
}).catch(done.fail); }).catch(done.fail);
}); });
it('closes all files', (done) => {
store.state.openFiles.push(file());
store.state.openFiles[0].opened = true;
store.dispatch('commitChanges', { payload, newMr: false })
.then(Vue.nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('scrolls to top of page', (done) => { it('scrolls to top of page', (done) => {
store.dispatch('commitChanges', { payload, newMr: false }) store.dispatch('commitChanges', { payload, newMr: false })
.then(() => { .then(() => {
......
...@@ -16,7 +16,8 @@ describe('Loading Icon Component', () => { ...@@ -16,7 +16,8 @@ describe('Loading Icon Component', () => {
).toEqual('fa fa-spin fa-spinner fa-1x'); ).toEqual('fa fa-spin fa-spinner fa-1x');
expect(component.$el.tagName).toEqual('DIV'); expect(component.$el.tagName).toEqual('DIV');
expect(component.$el.classList.contains('text-center')).toEqual(true); expect(component.$el.classList).toContain('text-center');
expect(component.$el.classList).toContain('loading-container');
}); });
it('should render accessibility attributes', () => { it('should render accessibility attributes', () => {
......
...@@ -278,18 +278,19 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -278,18 +278,19 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com' expect(doc.at_css('a')['href']).to eq 'http://example.com'
end end
it 'supports Unicode filenames' do it 'supports unescaped Unicode filenames' do
path = '/uploads/한글.png' path = '/uploads/한글.png'
escaped = Addressable::URI.escape(path) doc = filter(link(path))
# Stub these methods so the file doesn't actually need to be in the repo expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
allow_any_instance_of(described_class) end
.to receive(:file_exists?).and_return(true)
allow_any_instance_of(described_class)
.to receive(:image?).with(path).and_return(true)
it 'supports escaped Unicode filenames' do
path = '/uploads/한글.png'
escaped = Addressable::URI.escape(path)
doc = filter(image(escaped)) doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match "/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png"
expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
end end
end end
......
...@@ -146,6 +146,12 @@ describe ProjectStatistics do ...@@ -146,6 +146,12 @@ describe ProjectStatistics do
expect(statistics.build_artifacts_size).to be(106365) expect(statistics.build_artifacts_size).to be(106365)
end end
it 'calculates related build artifacts by project' do
expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 }
statistics.update_build_artifacts_size
end
end end
context 'when legacy artifacts are used' do context 'when legacy artifacts are used' do
......
...@@ -79,6 +79,13 @@ describe Route do ...@@ -79,6 +79,13 @@ describe Route do
end end
describe 'callbacks' do describe 'callbacks' do
context 'before validation' do
it 'calls #delete_conflicting_orphaned_routes' do
expect(route).to receive(:delete_conflicting_orphaned_routes)
route.valid?
end
end
context 'after update' do context 'after update' do
it 'calls #create_redirect_for_old_path' do it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path) expect(route).to receive(:create_redirect_for_old_path)
...@@ -378,4 +385,58 @@ describe Route do ...@@ -378,4 +385,58 @@ describe Route do
end end
end end
end end
describe '#delete_conflicting_orphaned_routes' do
context 'when there is a conflicting route' do
let!(:conflicting_group) { create(:group, path: 'foo') }
before do
route.path = conflicting_group.route.path
end
context 'when the route is orphaned' do
let!(:offending_route) { conflicting_group.route }
before do
Group.delete(conflicting_group) # Orphan the route
end
it 'deletes the orphaned route' do
expect do
route.valid?
end.to change { described_class.count }.from(2).to(1)
end
it 'passes validation, as usual' do
expect(route.valid?).to be_truthy
end
end
context 'when the route is not orphaned' do
it 'does not delete the conflicting route' do
expect do
route.valid?
end.not_to change { described_class.count }
end
it 'fails validation, as usual' do
expect(route.valid?).to be_falsey
end
end
end
context 'when there are no conflicting routes' do
it 'does not delete any routes' do
route
expect do
route.valid?
end.not_to change { described_class.count }
end
it 'passes validation, as usual' do
expect(route.valid?).to be_truthy
end
end
end
end end
...@@ -59,7 +59,7 @@ describe API::Jobs do ...@@ -59,7 +59,7 @@ describe API::Jobs do
expect(json_job['pipeline']['status']).to eq job.pipeline.status expect(json_job['pipeline']['status']).to eq job.pipeline.status
end end
it 'avoids N+1 queries', skip_before_request: true do it 'avoids N+1 queries', :skip_before_request do
first_build = create(:ci_build, :artifacts, pipeline: pipeline) first_build = create(:ci_build, :artifacts, pipeline: pipeline)
first_build.runner = create(:ci_runner) first_build.runner = create(:ci_runner)
first_build.user = create(:user) first_build.user = create(:user)
......
...@@ -150,6 +150,19 @@ describe API::Projects do ...@@ -150,6 +150,19 @@ describe API::Projects do
expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
end end
context 'and with_issues_enabled=true' do
it 'only returns projects with issues enabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get api('/projects?with_issues_enabled=true', user)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).not_to include(project.id)
end
end
it "does not include statistics by default" do it "does not include statistics by default" do
get api('/projects', user) get api('/projects', user)
...@@ -352,6 +365,19 @@ describe API::Projects do ...@@ -352,6 +365,19 @@ describe API::Projects do
let(:current_user) { user2 } let(:current_user) { user2 }
let(:projects) { [public_project] } let(:projects) { [public_project] }
end end
context 'and with_issues_enabled=true' do
it 'does not return private issue projects' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
get api('/projects?with_issues_enabled=true', user2)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).not_to include(project.id)
end
end
end end
context 'when authenticated as admin' do context 'when authenticated as admin' do
......
...@@ -44,7 +44,7 @@ describe API::V3::Builds do ...@@ -44,7 +44,7 @@ describe API::V3::Builds do
expect(json_build['pipeline']['status']).to eq build.pipeline.status expect(json_build['pipeline']['status']).to eq build.pipeline.status
end end
it 'avoids N+1 queries', skip_before_request: true do it 'avoids N+1 queries', :skip_before_request do
first_build = create(:ci_build, :artifacts, pipeline: pipeline) first_build = create(:ci_build, :artifacts, pipeline: pipeline)
first_build.runner = create(:ci_runner) first_build.runner = create(:ci_runner)
first_build.user = create(:user) first_build.user = create(:user)
......
...@@ -3586,6 +3586,10 @@ html-entities@1.2.0, html-entities@^1.2.0: ...@@ -3586,6 +3586,10 @@ html-entities@1.2.0, html-entities@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
htmlescape@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
htmlparser2@^3.8.2, htmlparser2@^3.9.0: htmlparser2@^3.8.2, htmlparser2@^3.9.0:
version "3.9.2" version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
...@@ -4633,6 +4637,10 @@ lodash.memoize@^4.1.2: ...@@ -4633,6 +4637,10 @@ lodash.memoize@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
lodash.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
lodash.mergewith@^4.6.0: lodash.mergewith@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
......
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