Commit 6b89ab11 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'tz-ide-open-mr' into 'master'

Basic Setup for MR Showing

Closes #44840 and #44839

See merge request gitlab-org/gitlab-ce!17952
parents d2ad0bcc 9d958af5
...@@ -10,6 +10,9 @@ const Api = { ...@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
...@@ -22,25 +25,27 @@ const Api = { ...@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
.replace(':id', groupId); return axios.get(url).then(({ data }) => {
return axios.get(url) callback(data);
.then(({ data }) => {
callback(data);
return data; return data;
}); });
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
groups(query, options, callback = $.noop) { groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath); const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, { return axios
params: Object.assign({ .get(url, {
search: query, params: Object.assign(
per_page: 20, {
}, options), search: query,
}) per_page: 20,
},
options,
),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -51,12 +56,13 @@ const Api = { ...@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query // Return namespaces list. Filtered by query
namespaces(query, callback) { namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath); const url = Api.buildUrl(Api.namespacesPath);
return axios.get(url, { return axios
params: { .get(url, {
search: query, params: {
per_page: 20, search: query,
}, per_page: 20,
}) },
})
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
...@@ -73,9 +79,10 @@ const Api = { ...@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true; defaults.membership = true;
} }
return axios.get(url, { return axios
params: Object.assign(defaults, options), .get(url, {
}) params: Object.assign(defaults, options),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -85,8 +92,32 @@ const Api = { ...@@ -85,8 +92,32 @@ const Api = {
// Return single project // Return single project
project(projectPath) { project(projectPath) {
const url = Api.buildUrl(Api.projectPath) const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
.replace(':id', encodeURIComponent(projectPath));
return axios.get(url);
},
// Return Merge Request for project
mergeRequest(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
mergeRequestChanges(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
mergeRequestVersions(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestVersionsPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url); return axios.get(url);
}, },
...@@ -102,30 +133,30 @@ const Api = { ...@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
} }
return axios.post(url, { return axios
label: data, .post(url, {
}) label: data,
})
.then(res => callback(res.data)) .then(res => callback(res.data))
.catch(e => callback(e.response.data)); .catch(e => callback(e.response.data));
}, },
// Return group projects list. Filtered by query // Return group projects list. Filtered by query
groupProjects(groupId, query, callback) { groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath) const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
.replace(':id', groupId); return axios
return axios.get(url, { .get(url, {
params: { params: {
search: query, search: query,
per_page: 20, per_page: 20,
}, },
}) })
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
commitMultiple(id, data) { commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath) const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
.replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), { return axios.post(url, JSON.stringify(data), {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
...@@ -136,39 +167,34 @@ const Api = { ...@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) { branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath) const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id)) .replace(':id', encodeURIComponent(id))
.replace(':branch', branch); .replace(':branch', encodeURIComponent(branch));
return axios.get(url); return axios.get(url);
}, },
// Return text for a specific license // Return text for a specific license
licenseText(key, data, callback) { licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath) const url = Api.buildUrl(Api.licensePath).replace(':key', key);
.replace(':key', key); return axios
return axios.get(url, { .get(url, {
params: data, params: data,
}) })
.then(res => callback(res.data)); .then(res => callback(res.data));
}, },
gitignoreText(key, callback) { gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath) const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
.replace(':key', key); return axios.get(url).then(({ data }) => callback(data));
return axios.get(url)
.then(({ data }) => callback(data));
}, },
gitlabCiYml(key, callback) { gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath) const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
.replace(':key', key); return axios.get(url).then(({ data }) => callback(data));
return axios.get(url)
.then(({ data }) => callback(data));
}, },
dockerfileYml(key, callback) { dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
return axios.get(url) return axios.get(url).then(({ data }) => callback(data));
.then(({ data }) => callback(data));
}, },
issueTemplate(namespacePath, projectPath, key, type, callback) { issueTemplate(namespacePath, projectPath, key, type, callback) {
...@@ -177,7 +203,8 @@ const Api = { ...@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type) .replace(':type', type)
.replace(':project_path', projectPath) .replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath); .replace(':namespace_path', namespacePath);
return axios.get(url) return axios
.get(url)
.then(({ data }) => callback(null, data)) .then(({ data }) => callback(null, data))
.catch(callback); .catch(callback);
}, },
...@@ -185,10 +212,13 @@ const Api = { ...@@ -185,10 +212,13 @@ const Api = {
users(query, options) { users(query, options) {
const url = Api.buildUrl(this.usersPath); const url = Api.buildUrl(this.usersPath);
return axios.get(url, { return axios.get(url, {
params: Object.assign({ params: Object.assign(
search: query, {
per_page: 20, search: query,
}, options), per_page: 20,
},
options,
),
}); });
}, },
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
icon, icon,
},
props: {
file: {
type: Object,
required: true,
}, },
props: { },
file: { computed: {
type: Object, changedIcon() {
required: true, return this.file.tempFile ? 'file-addition' : 'file-modified';
},
}, },
computed: { changedIconClass() {
changedIcon() { return `multi-${this.changedIcon}`;
return this.file.tempFile ? 'file-addition' : 'file-modified';
},
changedIconClass() {
return `multi-${this.changedIcon}`;
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
export default { export default {
components: { components: {
Icon, Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
}, },
props: { mergeRequestId: {
hasChanges: { type: String,
type: Boolean, required: false,
required: false, default: '',
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
}, },
methods: { viewer: {
changeMode(mode) { type: String,
this.$emit('click', mode); required: true,
},
}, },
}; showShadow: {
type: Boolean,
required: true,
},
},
computed: {
mergeReviewLine() {
return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
mergeRequestId: this.mergeRequestId,
});
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script> </script>
<template> <template>
...@@ -43,7 +56,10 @@ ...@@ -43,7 +56,10 @@
}" }"
data-toggle="dropdown" data-toggle="dropdown"
> >
<template v-if="viewer === 'editor'"> <template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }} {{ __('Editing') }}
</template> </template>
<template v-else> <template v-else>
...@@ -57,6 +73,29 @@ ...@@ -57,6 +73,29 @@
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul> <ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li> <li>
<a <a
href="#" href="#"
......
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue'; import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
components: { components: {
ideSidebar, ideSidebar,
ideContextbar, ideContextbar,
repoTabs, repoTabs,
repoFileButtons, repoFileButtons,
ideStatusBar, ideStatusBar,
repoEditor, repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
}, },
props: { noChangesStateSvgPath: {
emptyStateSvgPath: { type: String,
type: String, required: true,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
}, },
computed: { committedStateSvgPath: {
...mapState(['changedFiles', 'openFiles', 'viewer']), type: String,
...mapGetters(['activeFile', 'hasChanges']), required: true,
}, },
mounted() { },
const returnValue = 'Are you sure you want to lose unsaved changes?'; computed: {
window.onbeforeunload = e => { ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
if (!this.changedFiles.length) return undefined; ...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, { Object.assign(e, {
returnValue, returnValue,
}); });
return returnValue; return returnValue;
}; };
}, },
}; };
</script> </script>
<template> <template>
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"
:has-changes="hasChanges" :has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/> />
<repo-editor <repo-editor
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
icon,
},
directives: {
tooltip,
},
};
</script>
<template>
<icon
name="git-merge"
v-tooltip
title="__('Part of merge request changes')"
css-classes="ide-file-changed-icon"
:size="12"
/>
</template>
<script> <script>
/* global monaco */ /* global monaco */
import { mapState, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
...@@ -13,12 +13,8 @@ export default { ...@@ -13,12 +13,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapState([ ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
'leftPanelCollapsed', ...mapGetters(['currentMergeRequest']),
'rightPanelCollapsed',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw; return this.file && this.file.binary && !this.file.raw;
}, },
...@@ -68,9 +64,14 @@ export default { ...@@ -68,9 +64,14 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.getRawFileData(this.file) this.getRawFileData({
path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve(); const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor')
: Promise.resolve();
return viewerPromise; return viewerPromise;
}) })
...@@ -78,7 +79,7 @@ export default { ...@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false); this.updateDelayViewerUpdated(false);
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch((err) => { .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err; throw err;
}); });
...@@ -101,9 +102,13 @@ export default { ...@@ -101,9 +102,13 @@ export default {
this.model = this.editor.createModel(this.file); this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model); if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
}
this.model.onChange((model) => { this.model.onChange(model => {
const { file } = model; const { file } = model;
if (file.active) { if (file.active) {
......
...@@ -6,6 +6,7 @@ import router from '../ide_router'; ...@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue'; import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import changedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue';
export default { export default {
name: 'RepoFile', name: 'RepoFile',
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
fileStatusIcon, fileStatusIcon,
fileIcon, fileIcon,
changedFileIcon, changedFileIcon,
mrFileIcon,
}, },
props: { props: {
file: { file: {
...@@ -56,10 +58,7 @@ export default { ...@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if ( if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
...@@ -102,11 +101,15 @@ export default { ...@@ -102,11 +101,15 @@ export default {
:file="file" :file="file"
/> />
</span> </span>
<changed-file-icon <span class="pull-right">
:file="file" <mr-file-icon
v-if="file.changed || file.tempFile" v-if="file.mrChange"
class="prepend-top-5 pull-right" />
/> <changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
/>
</span>
<new-dropdown <new-dropdown
v-if="isTree" v-if="isTree"
:project-id="file.projectId" :project-id="file.projectId"
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
export default { export default {
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
}, },
directives: { },
tooltip, computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
}, },
props: { },
file: { };
type: Object,
required: true,
},
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue'; import EditorMode from './editor_mode_dropdown.vue';
export default { export default {
components: { components: {
RepoTab, RepoTab,
EditorMode, EditorMode,
},
props: {
files: {
type: Array,
required: true,
}, },
props: { viewer: {
files: { type: String,
type: Array, required: true,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
}, },
data() { hasChanges: {
return { type: Boolean,
showShadow: false, required: true,
};
}, },
updated() { mergeRequestId: {
if (!this.$refs.tabsScroller) return; type: String,
required: false,
this.showShadow = default: '',
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
}, },
}; },
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
},
};
</script> </script>
<template> <template>
...@@ -55,6 +59,7 @@ ...@@ -55,6 +59,7 @@
:viewer="viewer" :viewer="viewer"
:show-shadow="showShadow" :show-shadow="showShadow"
:has-changes="hasChanges" :has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="updateViewer" @click="updateViewer"
/> />
</div> </div>
......
...@@ -44,7 +44,7 @@ const router = new VueRouter({ ...@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
{ {
path: 'mr/:mrid', path: 'merge_requests/:mrid',
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
], ],
...@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => { ...@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => { .then(() => {
if (to.params[0]) { if (to.params[0]) {
const path = const path =
to.params[0].slice(-1) === '/' to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
? to.params[0].slice(0, -1)
: to.params[0];
const treeEntry = store.state.entries[path]; const treeEntry = store.state.entries[path];
if (treeEntry) { if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry); store.dispatch('handleTreeEntryAction', treeEntry);
...@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => { ...@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
); );
throw e; throw e;
}); });
} else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
return store.dispatch('getFiles', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
})
.then(() =>
store.dispatch('getMergeRequestVersions', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
}),
)
.then(() =>
store.dispatch('getMergeRequestChanges', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
}),
)
.then(mrChanges => {
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = store.state.entries[change.new_path];
if (changeTreeEntry) {
store.dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
store.dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash('Error while loading the merge request. Please try again.');
throw e;
});
} }
}) })
.catch(e => { .catch(e => {
......
...@@ -21,6 +21,15 @@ export default class Model { ...@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.path), new this.monaco.Uri(null, null, this.file.path),
)), )),
); );
if (this.file.mrChange) {
this.disposable.add(
(this.baseModel = this.monaco.editor.createModel(
this.file.baseRaw,
undefined,
new this.monaco.Uri(null, null, `target/${this.file.path}`),
)),
);
}
this.events = new Map(); this.events = new Map();
...@@ -28,10 +37,7 @@ export default class Model { ...@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this); this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on( eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
} }
get url() { get url() {
...@@ -58,6 +64,10 @@ export default class Model { ...@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel; return this.originalModel;
} }
getBaseModel() {
return this.baseModel;
}
setValue(value) { setValue(value) {
this.getModel().setValue(value); this.getModel().setValue(value);
} }
...@@ -78,13 +88,7 @@ export default class Model { ...@@ -78,13 +88,7 @@ export default class Model {
this.disposable.dispose(); this.disposable.dispose();
this.events.clear(); this.events.clear();
eventHub.$off( eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
`editor.update.model.dispose.${this.file.path}`, eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
this.dispose,
);
eventHub.$off(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
} }
} }
...@@ -109,11 +109,19 @@ export default class Editor { ...@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
attachMergeRequestModel(model) {
this.instance.setModel({
original: model.getBaseModel(),
modified: model.getModel(),
});
this.monaco.editor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true,
});
}
setupMonacoTheme() { setupMonacoTheme() {
this.monaco.editor.defineTheme( this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab'); this.monaco.editor.setTheme('gitlab');
} }
...@@ -161,8 +169,6 @@ export default class Editor { ...@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) { onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return; if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add( this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
} }
} }
...@@ -20,12 +20,35 @@ export default { ...@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw); return Promise.resolve(file.raw);
} }
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
},
getBaseRawFileData(file, sha) {
if (file.tempFile) {
return Promise.resolve(file.baseRaw);
}
if (file.baseRaw) {
return Promise.resolve(file.baseRaw);
}
return Vue.http
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' },
})
.then(res => res.text()); .then(res => res.text());
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`); return Api.project(`${namespace}/${project}`);
}, },
getProjectMergeRequestData(projectId, mergeRequestId) {
return Api.mergeRequest(projectId, mergeRequestId);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
},
getProjectMergeRequestVersions(projectId, mergeRequestId) {
return Api.mergeRequestVersions(projectId, mergeRequestId);
},
getBranchData(projectId, currentBranchId) { getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId); return Api.branchSingle(projectId, currentBranchId);
}, },
......
...@@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker'; ...@@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => { export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => { state.changedFiles.forEach(file => {
...@@ -43,14 +42,11 @@ export const createTempEntry = ( ...@@ -43,14 +42,11 @@ export const createTempEntry = (
) => ) =>
new Promise(resolve => { new Promise(resolve => {
const worker = new FilesDecoratorWorker(); const worker = new FilesDecoratorWorker();
const fullName = const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) { if (state.entries[name]) {
flash( flash(
`The name "${name `The name "${name.split('/').pop()}" is already taken in this directory.`,
.split('/')
.pop()}" is already taken in this directory.`,
'alert', 'alert',
document, document,
null, null,
...@@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
export * from './actions/merge_request';
...@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId); commit(types.SET_CURRENT_BRANCH, file.branchId);
}; };
export const getFileData = ({ state, commit, dispatch }, file) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
return service return service
.getFileData(file.url) .getFileData(file.url)
.then(res => { .then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle); setPageTitle(pageTitle);
return res.json(); return res.json();
}) })
.then(data => { .then(data => {
commit(types.SET_FILE_DATA, { data, file }); commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path); commit(types.TOGGLE_FILE_OPEN, path);
dispatch('setFileActive', file.path); if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
flash( flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
}); });
}; };
export const getRawFileData = ({ commit, dispatch }, file) => export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
service commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
.getRawFileData(file) };
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
}) const file = state.entries[path];
.catch(() => return new Promise((resolve, reject) => {
flash( service
'Error loading file content. Please try again.', .getRawFileData(file)
'alert', .then(raw => {
document, commit(types.SET_FILE_RAW_DATA, { file, raw });
null, if (file.mrChange && file.mrChange.new_file === false) {
false, service
true, .getBaseRawFileData(file, baseSha)
), .then(baseRaw => {
); commit(types.SET_FILE_BASE_RAW_DATA, {
file,
baseRaw,
});
resolve(raw);
})
.catch(e => {
reject(e);
});
} else {
resolve(raw);
}
})
.catch(() => {
flash('Error loading file content. Please try again.');
reject();
});
});
};
export const changeFileContent = ({ state, commit }, { path, content }) => { export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
...@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => { ...@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
} }
}; };
export const setEditorPosition = ( export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
{ getters, commit },
{ editorRow, editorColumn },
) => {
if (getters.activeFile) { if (getters.activeFile) {
commit(types.SET_FILE_POSITION, { commit(types.SET_FILE_POSITION, {
file: getters.activeFile, file: getters.activeFile,
......
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
.getProjectMergeRequestData(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
mergeRequest: data,
});
if (!state.currentMergeRequestId) {
commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
}
resolve(data);
})
.catch(() => {
flash('Error loading merge request data. Please try again.');
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
}
});
export const getMergeRequestChanges = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
changes: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request changes. Please try again.');
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
}
});
export const getMergeRequestVersions = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
service
.getProjectMergeRequestVersions(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_VERSIONS, {
projectPath: projectId,
mergeRequestId,
versions: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request versions. Please try again.');
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
}
});
...@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils'; ...@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
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 { import { findEntry } from '../utils';
findEntry,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker'; import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => { export const toggleTreeOpen = ({ commit, dispatch }, path) => {
...@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { ...@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path); dispatch('setFileActive', row.path);
} else { } else {
dispatch('getFileData', row); dispatch('getFileData', { path: row.path });
} }
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath) service
.then((res) => { .getTreeLastCommit(tree.lastCommitPath)
.then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json(); return res.json();
}) })
.then((data) => { .then(data => {
data.forEach((lastCommit) => { data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name); const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) { if (entry) {
...@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const getFiles = ( export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
{ state, commit, dispatch }, new Promise((resolve, reject) => {
{ projectId, branchId } = {}, if (!state.trees[`${projectId}/${branchId}`]) {
) => new Promise((resolve, reject) => { const selectedProject = state.projects[projectId];
if (!state.trees[`${projectId}/${branchId}`]) { commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); service
.getFiles(selectedProject.web_url, branchId)
service .then(res => res.json())
.getFiles(selectedProject.web_url, branchId) .then(data => {
.then(res => res.json()) const worker = new FilesDecoratorWorker();
.then((data) => { worker.addEventListener('message', e => {
const worker = new FilesDecoratorWorker(); const { entries, treeList } = e.data;
worker.addEventListener('message', (e) => { const selectedTree = state.trees[`${projectId}/${branchId}`];
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`]; commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, {
commit(types.SET_ENTRIES, entries); treePath: `${projectId}/${branchId}`,
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList }); data: treeList,
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false }); });
commit(types.TOGGLE_LOADING, {
worker.terminate(); entry: selectedTree,
forceValue: false,
resolve(); });
});
worker.terminate();
worker.postMessage({
data, resolve();
projectId, });
branchId,
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch(e => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
}); });
}) } else {
.catch((e) => { resolve();
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); }
reject(e); });
});
} else {
resolve();
}
});
export const activeFile = state => export const activeFile = state => state.openFiles.find(file => file.active) || null;
state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state => export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => { Object.keys(state.projects).map(projectId => {
...@@ -23,8 +21,17 @@ export const projectsWithTrees = state => ...@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
}; };
}); });
export const currentMergeRequest = state => {
if (state.projects[state.currentProjectId]) {
return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
}
return null;
};
// eslint-disable-next-line no-confusing-arrow // eslint-disable-next-line no-confusing-arrow
export const currentIcon = state => export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length; export const hasChanges = state => !!state.changedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
...@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT'; ...@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types // Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
...@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA'; ...@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION'; export const SET_FILE_POSITION = 'SET_FILE_POSITION';
...@@ -39,5 +46,6 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; ...@@ -39,5 +46,6 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES'; export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER'; export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
...@@ -11,10 +12,7 @@ export default { ...@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) { if (entry.path) {
Object.assign(state.entries[entry.path], { Object.assign(state.entries[entry.path], {
loading: loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
}); });
} else { } else {
Object.assign(entry, { Object.assign(entry, {
...@@ -83,9 +81,7 @@ export default { ...@@ -83,9 +81,7 @@ export default {
if (!foundEntry) { if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], { Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat( tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
data.treeList,
),
}); });
} }
}, },
...@@ -100,6 +96,7 @@ export default { ...@@ -100,6 +96,7 @@ export default {
}); });
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
...branchMutations, ...branchMutations,
......
...@@ -28,6 +28,8 @@ export default { ...@@ -28,6 +28,8 @@ export default {
rawPath: data.raw_path, rawPath: data.raw_path,
binary: data.binary, binary: data.binary,
renderError: data.render_error, renderError: data.render_error,
raw: null,
baseRaw: null,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
...@@ -35,6 +37,11 @@ export default { ...@@ -35,6 +37,11 @@ export default {
raw, raw,
}); });
}, },
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], {
baseRaw,
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) { [types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw; const changed = content !== state.entries[path].raw;
...@@ -59,6 +66,11 @@ export default { ...@@ -59,6 +66,11 @@ export default {
editorColumn, editorColumn,
}); });
}, },
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
Object.assign(state.entries[file.path], {
mrChange,
});
},
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: state.entries[path].raw, content: state.entries[path].raw,
......
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
Object.assign(state, {
currentMergeRequestId,
});
},
[types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
Object.assign(state.projects[projectPath], {
mergeRequests: {
[mergeRequestId]: {
...mergeRequest,
active: true,
changes: [],
versions: [],
baseCommitSha: null,
},
},
});
},
[types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
changes,
});
},
[types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
versions,
baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
});
},
};
...@@ -11,6 +11,7 @@ export default { ...@@ -11,6 +11,7 @@ export default {
Object.assign(project, { Object.assign(project, {
tree: [], tree: [],
branches: {}, branches: {},
mergeRequests: {},
active: true, active: true,
}); });
......
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [], changedFiles: [],
endpoints: {}, endpoints: {},
lastCommitMsg: '', lastCommitMsg: '',
......
...@@ -38,7 +38,7 @@ export const dataStructure = () => ({ ...@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '', eol: '',
}); });
export const decorateData = (entity) => { export const decorateData = entity => {
const { const {
id, id,
projectId, projectId,
...@@ -57,7 +57,6 @@ export const decorateData = (entity) => { ...@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false, base64 = false,
file_lock, file_lock,
} = entity; } = entity;
return { return {
...@@ -80,17 +79,15 @@ export const decorateData = (entity) => { ...@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
base64, base64,
file_lock, file_lock,
}; };
}; };
export const findEntry = (tree, type, name, prop = 'name') => tree.find( export const findEntry = (tree, type, name, prop = 'name') =>
f => f.type === type && f[prop] === name, tree.find(f => f.type === type && f[prop] === name);
);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => { export const setPageTitle = title => {
document.title = title; document.title = title;
}; };
...@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => { ...@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0; return 0;
}; };
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, { export const sortTree = sortedTree =>
tree: entity.tree.length ? sortTree(entity.tree) : [], sortedTree
})).sort(sortTreesByTypeAndName); .map(entity =>
Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
}),
)
.sort(sortTreesByTypeAndName);
...@@ -51,7 +51,7 @@ export function removeParams(params) { ...@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a'); const url = document.createElement('a');
url.href = window.location.href; url.href = window.location.href;
params.forEach((param) => { params.forEach(param => {
url.search = removeParamQueryString(url.search, param); url.search = removeParamQueryString(url.search, param);
}); });
...@@ -83,3 +83,11 @@ export function refreshCurrentPage() { ...@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) { export function redirectTo(url) {
return window.location.assign(url); return window.location.assign(url);
} }
export function webIDEUrl(route = undefined) {
let returnUrl = `${gon.relative_url_root}/-/ide/`;
if (route) {
returnUrl += `project${route}`;
}
return returnUrl;
}
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue'; import { webIDEUrl } from '~/lib/utils/url_utility';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
}, },
components: { },
icon, computed: {
clipboardButton, shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
}, },
props: { commitsText() {
mr: { return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
type: Object,
required: true,
},
}, },
computed: { branchNameClipboardData() {
shouldShowCommitsBehindText() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that
return this.mr.divergedCommitsCount > 0; // works around ClipboardJS limitations to allow the context-specific
}, // copy/pasting of plain text or GFM.
commitsText() { return JSON.stringify({
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); text: this.mr.sourceBranch,
}, gfm: `\`${this.mr.sourceBranch}\``,
branchNameClipboardData() { });
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
}, },
methods: { isSourceBranchLong() {
isBranchTitleLong(branchTitle) { return this.isBranchTitleLong(this.mr.sourceBranch);
return branchTitle.length > 32;
},
}, },
}; isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() {
return webIDEUrl(this.mr.statusPath.replace('.json', ''));
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script> </script>
<template> <template>
<div class="mr-source-target"> <div class="mr-source-target">
...@@ -96,6 +100,13 @@ ...@@ -96,6 +100,13 @@
</div> </div>
<div v-if="mr.isOpen"> <div v-if="mr.isOpen">
<a
v-if="!mr.sourceBranchRemoved"
:href="webIdePath"
class="btn btn-sm btn-default inline js-web-ide"
>
{{ s__("mrWidget|Web IDE") }}
</a>
<button <button
data-target="#modal_merge_info" data-target="#modal_merge_info"
data-toggle="modal" data-toggle="modal"
......
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
flex: 1; flex: 1;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: inherit;
svg { svg {
vertical-align: middle; vertical-align: middle;
......
...@@ -35,14 +35,14 @@ describe('Api', () => { ...@@ -35,14 +35,14 @@ describe('Api', () => {
}); });
describe('group', () => { describe('group', () => {
it('fetches a group', (done) => { it('fetches a group', done => {
const groupId = '123456'; const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
mock.onGet(expectedUrl).reply(200, { mock.onGet(expectedUrl).reply(200, {
name: 'test', name: 'test',
}); });
Api.group(groupId, (response) => { Api.group(groupId, response => {
expect(response.name).toBe('test'); expect(response.name).toBe('test');
done(); done();
}); });
...@@ -50,15 +50,17 @@ describe('Api', () => { ...@@ -50,15 +50,17 @@ describe('Api', () => {
}); });
describe('groups', () => { describe('groups', () => {
it('fetches groups', (done) => { it('fetches groups', done => {
const query = 'dummy query'; const query = 'dummy query';
const options = { unused: 'option' }; const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
mock.onGet(expectedUrl).reply(200, [{ mock.onGet(expectedUrl).reply(200, [
name: 'test', {
}]); name: 'test',
},
]);
Api.groups(query, options, (response) => { Api.groups(query, options, response => {
expect(response.length).toBe(1); expect(response.length).toBe(1);
expect(response[0].name).toBe('test'); expect(response[0].name).toBe('test');
done(); done();
...@@ -67,14 +69,16 @@ describe('Api', () => { ...@@ -67,14 +69,16 @@ describe('Api', () => {
}); });
describe('namespaces', () => { describe('namespaces', () => {
it('fetches namespaces', (done) => { it('fetches namespaces', done => {
const query = 'dummy query'; const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
mock.onGet(expectedUrl).reply(200, [{ mock.onGet(expectedUrl).reply(200, [
name: 'test', {
}]); name: 'test',
},
]);
Api.namespaces(query, (response) => { Api.namespaces(query, response => {
expect(response.length).toBe(1); expect(response.length).toBe(1);
expect(response[0].name).toBe('test'); expect(response[0].name).toBe('test');
done(); done();
...@@ -83,31 +87,35 @@ describe('Api', () => { ...@@ -83,31 +87,35 @@ describe('Api', () => {
}); });
describe('projects', () => { describe('projects', () => {
it('fetches projects with membership when logged in', (done) => { it('fetches projects with membership when logged in', done => {
const query = 'dummy query'; const query = 'dummy query';
const options = { unused: 'option' }; const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1; window.gon.current_user_id = 1;
mock.onGet(expectedUrl).reply(200, [{ mock.onGet(expectedUrl).reply(200, [
name: 'test', {
}]); name: 'test',
},
]);
Api.projects(query, options, (response) => { Api.projects(query, options, response => {
expect(response.length).toBe(1); expect(response.length).toBe(1);
expect(response[0].name).toBe('test'); expect(response[0].name).toBe('test');
done(); done();
}); });
}); });
it('fetches projects without membership when not logged in', (done) => { it('fetches projects without membership when not logged in', done => {
const query = 'dummy query'; const query = 'dummy query';
const options = { unused: 'option' }; const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
mock.onGet(expectedUrl).reply(200, [{ mock.onGet(expectedUrl).reply(200, [
name: 'test', {
}]); name: 'test',
},
]);
Api.projects(query, options, (response) => { Api.projects(query, options, response => {
expect(response.length).toBe(1); expect(response.length).toBe(1);
expect(response[0].name).toBe('test'); expect(response[0].name).toBe('test');
done(); done();
...@@ -115,8 +123,65 @@ describe('Api', () => { ...@@ -115,8 +123,65 @@ describe('Api', () => {
}); });
}); });
describe('mergerequest', () => {
it('fetches a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
mock.onGet(expectedUrl).reply(200, {
title: 'test',
});
Api.mergeRequest(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.title).toBe('test');
})
.then(done)
.catch(done.fail);
});
});
describe('mergerequest changes', () => {
it('fetches the changes of a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
mock.onGet(expectedUrl).reply(200, {
title: 'test',
});
Api.mergeRequestChanges(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.title).toBe('test');
})
.then(done)
.catch(done.fail);
});
});
describe('mergerequest versions', () => {
it('fetches the versions of a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
mock.onGet(expectedUrl).reply(200, [
{
id: 123,
},
]);
Api.mergeRequestVersions(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.length).toBe(1);
expect(data[0].id).toBe(123);
})
.then(done)
.catch(done.fail);
});
});
describe('newLabel', () => { describe('newLabel', () => {
it('creates a new label', (done) => { it('creates a new label', done => {
const namespace = 'some namespace'; const namespace = 'some namespace';
const project = 'some project'; const project = 'some project';
const labelData = { some: 'data' }; const labelData = { some: 'data' };
...@@ -124,36 +189,42 @@ describe('Api', () => { ...@@ -124,36 +189,42 @@ describe('Api', () => {
const expectedData = { const expectedData = {
label: labelData, label: labelData,
}; };
mock.onPost(expectedUrl).reply((config) => { mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData)); expect(config.data).toBe(JSON.stringify(expectedData));
return [200, { return [
name: 'test', 200,
}]; {
name: 'test',
},
];
}); });
Api.newLabel(namespace, project, labelData, (response) => { Api.newLabel(namespace, project, labelData, response => {
expect(response.name).toBe('test'); expect(response.name).toBe('test');
done(); done();
}); });
}); });
it('creates a group label', (done) => { it('creates a group label', done => {
const namespace = 'group/subgroup'; const namespace = 'group/subgroup';
const labelData = { some: 'data' }; const labelData = { some: 'data' };
const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`; const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
const expectedData = { const expectedData = {
label: labelData, label: labelData,
}; };
mock.onPost(expectedUrl).reply((config) => { mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData)); expect(config.data).toBe(JSON.stringify(expectedData));
return [200, { return [
name: 'test', 200,
}]; {
name: 'test',
},
];
}); });
Api.newLabel(namespace, undefined, labelData, (response) => { Api.newLabel(namespace, undefined, labelData, response => {
expect(response.name).toBe('test'); expect(response.name).toBe('test');
done(); done();
}); });
...@@ -161,15 +232,17 @@ describe('Api', () => { ...@@ -161,15 +232,17 @@ describe('Api', () => {
}); });
describe('groupProjects', () => { describe('groupProjects', () => {
it('fetches group projects', (done) => { it('fetches group projects', done => {
const groupId = '123456'; const groupId = '123456';
const query = 'dummy query'; const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
mock.onGet(expectedUrl).reply(200, [{ mock.onGet(expectedUrl).reply(200, [
name: 'test', {
}]); name: 'test',
},
]);
Api.groupProjects(groupId, query, (response) => { Api.groupProjects(groupId, query, response => {
expect(response.length).toBe(1); expect(response.length).toBe(1);
expect(response[0].name).toBe('test'); expect(response[0].name).toBe('test');
done(); done();
...@@ -178,13 +251,13 @@ describe('Api', () => { ...@@ -178,13 +251,13 @@ describe('Api', () => {
}); });
describe('licenseText', () => { describe('licenseText', () => {
it('fetches a license text', (done) => { it('fetches a license text', done => {
const licenseKey = "driver's license"; const licenseKey = "driver's license";
const data = { unused: 'option' }; const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
mock.onGet(expectedUrl).reply(200, 'test'); mock.onGet(expectedUrl).reply(200, 'test');
Api.licenseText(licenseKey, data, (response) => { Api.licenseText(licenseKey, data, response => {
expect(response).toBe('test'); expect(response).toBe('test');
done(); done();
}); });
...@@ -192,12 +265,12 @@ describe('Api', () => { ...@@ -192,12 +265,12 @@ describe('Api', () => {
}); });
describe('gitignoreText', () => { describe('gitignoreText', () => {
it('fetches a gitignore text', (done) => { it('fetches a gitignore text', done => {
const gitignoreKey = 'ignore git'; const gitignoreKey = 'ignore git';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
mock.onGet(expectedUrl).reply(200, 'test'); mock.onGet(expectedUrl).reply(200, 'test');
Api.gitignoreText(gitignoreKey, (response) => { Api.gitignoreText(gitignoreKey, response => {
expect(response).toBe('test'); expect(response).toBe('test');
done(); done();
}); });
...@@ -205,12 +278,12 @@ describe('Api', () => { ...@@ -205,12 +278,12 @@ describe('Api', () => {
}); });
describe('gitlabCiYml', () => { describe('gitlabCiYml', () => {
it('fetches a .gitlab-ci.yml', (done) => { it('fetches a .gitlab-ci.yml', done => {
const gitlabCiYmlKey = 'Y CI ML'; const gitlabCiYmlKey = 'Y CI ML';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test'); mock.onGet(expectedUrl).reply(200, 'test');
Api.gitlabCiYml(gitlabCiYmlKey, (response) => { Api.gitlabCiYml(gitlabCiYmlKey, response => {
expect(response).toBe('test'); expect(response).toBe('test');
done(); done();
}); });
...@@ -218,12 +291,12 @@ describe('Api', () => { ...@@ -218,12 +291,12 @@ describe('Api', () => {
}); });
describe('dockerfileYml', () => { describe('dockerfileYml', () => {
it('fetches a Dockerfile', (done) => { it('fetches a Dockerfile', done => {
const dockerfileYmlKey = 'a giant whale'; const dockerfileYmlKey = 'a giant whale';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test'); mock.onGet(expectedUrl).reply(200, 'test');
Api.dockerfileYml(dockerfileYmlKey, (response) => { Api.dockerfileYml(dockerfileYmlKey, response => {
expect(response).toBe('test'); expect(response).toBe('test');
done(); done();
}); });
...@@ -231,12 +304,14 @@ describe('Api', () => { ...@@ -231,12 +304,14 @@ describe('Api', () => {
}); });
describe('issueTemplate', () => { describe('issueTemplate', () => {
it('fetches an issue template', (done) => { it('fetches an issue template', done => {
const namespace = 'some namespace'; const namespace = 'some namespace';
const project = 'some project'; const project = 'some project';
const templateKey = ' template #%?.key '; const templateKey = ' template #%?.key ';
const templateType = 'template type'; const templateType = 'template type';
const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`; const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(
templateKey,
)}`;
mock.onGet(expectedUrl).reply(200, 'test'); mock.onGet(expectedUrl).reply(200, 'test');
Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
...@@ -247,13 +322,15 @@ describe('Api', () => { ...@@ -247,13 +322,15 @@ describe('Api', () => {
}); });
describe('users', () => { describe('users', () => {
it('fetches users', (done) => { it('fetches users', done => {
const query = 'dummy query'; const query = 'dummy query';
const options = { unused: 'option' }; const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
mock.onGet(expectedUrl).reply(200, [{ mock.onGet(expectedUrl).reply(200, [
name: 'test', {
}]); name: 'test',
},
]);
Api.users(query, options) Api.users(query, options)
.then(({ data }) => { .then(({ data }) => {
......
...@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => { ...@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => {
vm = createComponent(component, { vm = createComponent(component, {
file: { file: {
tempFile: false, tempFile: false,
changed: true,
}, },
}); });
}); });
...@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => { ...@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => {
}); });
describe('changedIcon', () => { describe('changedIcon', () => {
it('equals file-modified when not a temp file', () => { it('equals file-modified when not a temp file and has changes', () => {
expect(vm.changedIcon).toBe('file-modified'); expect(vm.changedIcon).toBe('file-modified');
}); });
......
...@@ -89,6 +89,20 @@ describe('RepoEditor', () => { ...@@ -89,6 +89,20 @@ describe('RepoEditor', () => {
done(); done();
}); });
}); });
it('calls createDiffInstance when viewer is a merge request diff', done => {
vm.$store.state.viewer = 'mrdiff';
spyOn(vm.editor, 'createDiffInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createDiffInstance).toHaveBeenCalled();
done();
});
});
}); });
describe('setupEditor', () => { describe('setupEditor', () => {
...@@ -134,4 +148,48 @@ describe('RepoEditor', () => { ...@@ -134,4 +148,48 @@ describe('RepoEditor', () => {
}); });
}); });
}); });
describe('setup editor for merge request viewing', () => {
beforeEach(done => {
// Resetting as the main test setup has already done it
vm.$destroy();
resetStore(vm.$store);
Editor.editorInstance.modelManager.dispose();
const f = {
...file(),
active: true,
tempFile: true,
html: 'testing',
mrChange: { diff: 'ABC' },
baseRaw: 'testing',
content: 'test',
};
const RepoEditor = Vue.extend(repoEditor);
vm = createComponentWithStore(RepoEditor, store, {
file: f,
});
vm.$store.state.openFiles.push(f);
vm.$store.state.entries[f.path] = f;
vm.$store.state.viewer = 'mrdiff';
vm.monaco = true;
vm.$mount();
monacoLoader(['vs/editor/editor.main'], () => {
setTimeout(done, 0);
});
});
it('attaches merge request model to editor when merge request diff', () => {
spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough();
vm.setupEditor();
expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
});
});
}); });
...@@ -17,6 +17,7 @@ describe('RepoTabs', () => { ...@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
files: openedFiles, files: openedFiles,
viewer: 'editor', viewer: 'editor',
hasChanges: false, hasChanges: false,
hasMergeRequest: false,
}); });
openedFiles[0].active = true; openedFiles[0].active = true;
...@@ -56,6 +57,7 @@ describe('RepoTabs', () => { ...@@ -56,6 +57,7 @@ describe('RepoTabs', () => {
files: [], files: [],
viewer: 'editor', viewer: 'editor',
hasChanges: false, hasChanges: false,
hasMergeRequest: false,
}, },
'#test-app', '#test-app',
); );
......
...@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => { ...@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => {
spyOn(eventHub, '$on').and.callThrough(); spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path')); const f = file('path');
f.mrChange = { diff: 'ABC' };
f.baseRaw = 'test';
model = new Model(monaco, f);
done(); done();
}); });
...@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => { ...@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => {
model.dispose(); model.dispose();
}); });
it('creates original model & new model', () => { it('creates original model & base model & new model', () => {
expect(model.originalModel).not.toBeNull(); expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull(); expect(model.model).not.toBeNull();
expect(model.baseModel).not.toBeNull();
}); });
it('adds eventHub listener', () => { it('adds eventHub listener', () => {
...@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => { ...@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => {
}); });
}); });
describe('getBaseModel', () => {
it('returns base model', () => {
expect(model.getBaseModel()).toBe(model.baseModel);
});
});
describe('setValue', () => { describe('setValue', () => {
it('updates models value', () => { it('updates models value', () => {
model.setValue('testing 123'); model.setValue('testing 123');
......
...@@ -143,6 +143,31 @@ describe('Multi-file editor library', () => { ...@@ -143,6 +143,31 @@ describe('Multi-file editor library', () => {
}); });
}); });
describe('attachMergeRequestModel', () => {
let model;
beforeEach(() => {
instance.createDiffInstance(document.createElement('div'));
const f = file();
f.mrChanges = { diff: 'ABC' };
f.baseRaw = 'testing';
model = instance.createModel(f);
});
it('sets original & modified', () => {
spyOn(instance.instance, 'setModel');
instance.attachMergeRequestModel(model);
expect(instance.instance.setModel).toHaveBeenCalledWith({
original: model.getBaseModel(),
modified: model.getModel(),
});
});
});
describe('clearEditor', () => { describe('clearEditor', () => {
it('resets the editor model', () => { it('resets the editor model', () => {
instance.createInstance(document.createElement('div')); instance.createInstance(document.createElement('div'));
......
...@@ -5,7 +5,7 @@ import router from '~/ide/ide_router'; ...@@ -5,7 +5,7 @@ import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers'; import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => { describe('IDE store file actions', () => {
beforeEach(() => { beforeEach(() => {
spyOn(router, 'push'); spyOn(router, 'push');
}); });
...@@ -189,7 +189,7 @@ describe('Multi-file store file actions', () => { ...@@ -189,7 +189,7 @@ describe('Multi-file store file actions', () => {
it('calls the service', done => { it('calls the service', done => {
store store
.dispatch('getFileData', localFile) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
...@@ -200,7 +200,7 @@ describe('Multi-file store file actions', () => { ...@@ -200,7 +200,7 @@ describe('Multi-file store file actions', () => {
it('sets the file data', done => { it('sets the file data', done => {
store store
.dispatch('getFileData', localFile) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(localFile.blamePath).toBe('blame_path'); expect(localFile.blamePath).toBe('blame_path');
...@@ -211,7 +211,7 @@ describe('Multi-file store file actions', () => { ...@@ -211,7 +211,7 @@ describe('Multi-file store file actions', () => {
it('sets document title', done => { it('sets document title', done => {
store store
.dispatch('getFileData', localFile) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(document.title).toBe('testing getFileData'); expect(document.title).toBe('testing getFileData');
...@@ -222,7 +222,7 @@ describe('Multi-file store file actions', () => { ...@@ -222,7 +222,7 @@ describe('Multi-file store file actions', () => {
it('sets the file as active', done => { it('sets the file as active', done => {
store store
.dispatch('getFileData', localFile) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(localFile.active).toBeTruthy(); expect(localFile.active).toBeTruthy();
...@@ -231,9 +231,20 @@ describe('Multi-file store file actions', () => { ...@@ -231,9 +231,20 @@ describe('Multi-file store file actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('sets the file not as active if we pass makeFileActive false', done => {
store
.dispatch('getFileData', { path: localFile.path, makeFileActive: false })
.then(() => {
expect(localFile.active).toBeFalsy();
done();
})
.catch(done.fail);
});
it('adds the file to open files', done => { it('adds the file to open files', done => {
store store
.dispatch('getFileData', localFile) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(store.state.openFiles.length).toBe(1); expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(localFile.name); expect(store.state.openFiles[0].name).toBe(localFile.name);
...@@ -256,7 +267,7 @@ describe('Multi-file store file actions', () => { ...@@ -256,7 +267,7 @@ describe('Multi-file store file actions', () => {
it('calls getRawFileData service method', done => { it('calls getRawFileData service method', done => {
store store
.dispatch('getRawFileData', tmpFile) .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => { .then(() => {
expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
...@@ -267,7 +278,7 @@ describe('Multi-file store file actions', () => { ...@@ -267,7 +278,7 @@ describe('Multi-file store file actions', () => {
it('updates file raw data', done => { it('updates file raw data', done => {
store store
.dispatch('getRawFileData', tmpFile) .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => { .then(() => {
expect(tmpFile.raw).toBe('raw'); expect(tmpFile.raw).toBe('raw');
...@@ -275,6 +286,22 @@ describe('Multi-file store file actions', () => { ...@@ -275,6 +286,22 @@ describe('Multi-file store file actions', () => {
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('calls also getBaseRawFileData service method', done => {
spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
tmpFile.mrChange = { new_file: false };
store
.dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
.then(() => {
expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
expect(tmpFile.baseRaw).toBe('baseraw');
done();
})
.catch(done.fail);
});
}); });
describe('changeFileContent', () => { describe('changeFileContent', () => {
......
import store from '~/ide/stores';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('IDE store merge request actions', () => {
beforeEach(() => {
store.state.projects.abcproject = {
mergeRequests: {},
};
});
afterEach(() => {
resetStore(store);
});
describe('getMergeRequestData', () => {
beforeEach(() => {
spyOn(service, 'getProjectMergeRequestData').and.returnValue(
Promise.resolve({ data: { title: 'mergerequest' } }),
);
});
it('calls getProjectMergeRequestData service method', done => {
store
.dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
done();
})
.catch(done.fail);
});
it('sets the Merge Request Object', done => {
store
.dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
expect(store.state.currentMergeRequestId).toBe(1);
done();
})
.catch(done.fail);
});
});
describe('getMergeRequestChanges', () => {
beforeEach(() => {
spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
Promise.resolve({ data: { title: 'mergerequest' } }),
);
store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
});
it('calls getProjectMergeRequestChanges service method', done => {
store
.dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
done();
})
.catch(done.fail);
});
it('sets the Merge Request Changes Object', done => {
store
.dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
'mergerequest',
);
done();
})
.catch(done.fail);
});
});
describe('getMergeRequestVersions', () => {
beforeEach(() => {
spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
Promise.resolve({ data: [{ id: 789 }] }),
);
store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
});
it('calls getProjectMergeRequestVersions service method', done => {
store
.dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
done();
})
.catch(done.fail);
});
it('sets the Merge Request Versions Object', done => {
store
.dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
done();
})
.catch(done.fail);
});
});
});
...@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => { ...@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => {
expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js'); expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
expect(projectTree.tree[1].type).toBe('blob'); expect(projectTree.tree[1].type).toBe('blob');
expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob'); expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
expect(projectTree.tree[0].tree[0].tree[0].name).toBe( expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
'fileinsubfolder.js',
);
done(); done();
}) })
...@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => { ...@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => {
store store
.dispatch('getLastCommitData', projectTree) .dispatch('getLastCommitData', projectTree)
.then(() => { .then(() => {
expect(service.getTreeLastCommit).toHaveBeenCalledWith( expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
'lastcommitpath',
);
done(); done();
}) })
...@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => { ...@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => {
.dispatch('getLastCommitData', projectTree) .dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick) .then(Vue.nextTick)
.then(() => { .then(() => {
expect(projectTree.tree[0].lastCommit.message).not.toBe( expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
'commit message',
);
done(); done();
}) })
......
...@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters'; ...@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters';
import state from '~/ide/stores/state'; import state from '~/ide/stores/state';
import { file } from '../helpers'; import { file } from '../helpers';
describe('Multi-file store getters', () => { describe('IDE store getters', () => {
let localState; let localState;
beforeEach(() => { beforeEach(() => {
...@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => { ...@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => {
expect(modifiedFiles[0].name).toBe('added'); expect(modifiedFiles[0].name).toBe('added');
}); });
}); });
describe('currentMergeRequest', () => {
it('returns Current Merge Request', () => {
localState.currentProjectId = 'abcproject';
localState.currentMergeRequestId = 1;
localState.projects.abcproject = {
mergeRequests: {
1: { mergeId: 1 },
},
};
expect(getters.currentMergeRequest(localState).mergeId).toBe(1);
});
it('returns null if no active Merge Request was found', () => {
localState.currentProjectId = 'otherproject';
expect(getters.currentMergeRequest(localState)).toBeNull();
});
});
}); });
...@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file'; ...@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state'; import state from '~/ide/stores/state';
import { file } from '../../helpers'; import { file } from '../../helpers';
describe('Multi-file store file mutations', () => { describe('IDE store file mutations', () => {
let localState; let localState;
let localFile; let localFile;
...@@ -62,6 +62,8 @@ describe('Multi-file store file mutations', () => { ...@@ -62,6 +62,8 @@ describe('Multi-file store file mutations', () => {
expect(localFile.rawPath).toBe('raw'); expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy(); expect(localFile.binary).toBeTruthy();
expect(localFile.renderError).toBe('render_error'); expect(localFile.renderError).toBe('render_error');
expect(localFile.raw).toBeNull();
expect(localFile.baseRaw).toBeNull();
}); });
}); });
...@@ -76,6 +78,17 @@ describe('Multi-file store file mutations', () => { ...@@ -76,6 +78,17 @@ describe('Multi-file store file mutations', () => {
}); });
}); });
describe('SET_FILE_BASE_RAW_DATA', () => {
it('sets raw data from base branch', () => {
mutations.SET_FILE_BASE_RAW_DATA(localState, {
file: localFile,
baseRaw: 'testing',
});
expect(localFile.baseRaw).toBe('testing');
});
});
describe('UPDATE_FILE_CONTENT', () => { describe('UPDATE_FILE_CONTENT', () => {
beforeEach(() => { beforeEach(() => {
localFile.raw = 'test'; localFile.raw = 'test';
...@@ -112,6 +125,17 @@ describe('Multi-file store file mutations', () => { ...@@ -112,6 +125,17 @@ describe('Multi-file store file mutations', () => {
}); });
}); });
describe('SET_FILE_MERGE_REQUEST_CHANGE', () => {
it('sets file mr change', () => {
mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
file: localFile,
mrChange: { diff: 'ABC' },
});
expect(localFile.mrChange.diff).toBe('ABC');
});
});
describe('DISCARD_FILE_CHANGES', () => { describe('DISCARD_FILE_CHANGES', () => {
beforeEach(() => { beforeEach(() => {
localFile.content = 'test'; localFile.content = 'test';
......
import mutations from '~/ide/stores/mutations/merge_request';
import state from '~/ide/stores/state';
describe('IDE store merge request mutations', () => {
let localState;
beforeEach(() => {
localState = state();
localState.projects = { abcproject: { mergeRequests: {} } };
mutations.SET_MERGE_REQUEST(localState, {
projectPath: 'abcproject',
mergeRequestId: 1,
mergeRequest: {
title: 'mr',
},
});
});
describe('SET_CURRENT_MERGE_REQUEST', () => {
it('sets current merge request', () => {
mutations.SET_CURRENT_MERGE_REQUEST(localState, 2);
expect(localState.currentMergeRequestId).toBe(2);
});
});
describe('SET_MERGE_REQUEST', () => {
it('setsmerge request data', () => {
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.title).toBe('mr');
expect(newMr.active).toBeTruthy();
});
});
describe('SET_MERGE_REQUEST_CHANGES', () => {
it('sets merge request changes', () => {
mutations.SET_MERGE_REQUEST_CHANGES(localState, {
projectPath: 'abcproject',
mergeRequestId: 1,
changes: {
diff: 'abc',
},
});
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.changes.diff).toBe('abc');
});
});
describe('SET_MERGE_REQUEST_VERSIONS', () => {
it('sets merge request versions', () => {
mutations.SET_MERGE_REQUEST_VERSIONS(localState, {
projectPath: 'abcproject',
mergeRequestId: 1,
versions: [{ id: 123 }],
});
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.versions.length).toBe(1);
expect(newMr.versions[0].id).toBe(123);
});
});
});
...@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => { ...@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => {
describe('computed', () => { describe('computed', () => {
describe('shouldShowCommitsBehindText', () => { describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => { it('return true when there are divergedCommitsCount', () => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 12, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 12,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', sourceBranch: 'mr-widget-refactor',
targetBranch: 'master', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
} }); targetBranch: 'master',
statusPath: 'abc',
},
});
expect(vm.shouldShowCommitsBehindText).toEqual(true); expect(vm.shouldShowCommitsBehindText).toEqual(true);
}); });
it('returns false where there are no divergedComits count', () => { it('returns false where there are no divergedComits count', () => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 0, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 0,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', sourceBranch: 'mr-widget-refactor',
targetBranch: 'master', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
} }); targetBranch: 'master',
statusPath: 'abc',
},
});
expect(vm.shouldShowCommitsBehindText).toEqual(false); expect(vm.shouldShowCommitsBehindText).toEqual(false);
}); });
}); });
describe('commitsText', () => { describe('commitsText', () => {
it('returns singular when there is one commit', () => { it('returns singular when there is one commit', () => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 1, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 1,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', sourceBranch: 'mr-widget-refactor',
targetBranch: 'master', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
} }); targetBranch: 'master',
statusPath: 'abc',
},
});
expect(vm.commitsText).toEqual('1 commit behind'); expect(vm.commitsText).toEqual('1 commit behind');
}); });
it('returns plural when there is more than one commit', () => { it('returns plural when there is more than one commit', () => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 2, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 2,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', sourceBranch: 'mr-widget-refactor',
targetBranch: 'master', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
} }); targetBranch: 'master',
statusPath: 'abc',
},
});
expect(vm.commitsText).toEqual('2 commits behind'); expect(vm.commitsText).toEqual('2 commits behind');
}); });
...@@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => { ...@@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => {
describe('template', () => { describe('template', () => {
describe('common elements', () => { describe('common elements', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 12, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 12,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', sourceBranch: 'mr-widget-refactor',
sourceBranchRemoved: false, sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
targetBranchPath: 'foo/bar/commits-path', sourceBranchRemoved: false,
targetBranchTreePath: 'foo/bar/tree/path', targetBranchPath: 'foo/bar/commits-path',
targetBranch: 'master', targetBranchTreePath: 'foo/bar/tree/path',
isOpen: true, targetBranch: 'master',
emailPatchesPath: '/mr/email-patches', isOpen: true,
plainDiffPath: '/mr/plainDiffPath', emailPatchesPath: '/mr/email-patches',
} }); plainDiffPath: '/mr/plainDiffPath',
statusPath: 'abc',
},
});
}); });
it('renders source branch link', () => { it('renders source branch link', () => {
expect( expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
vm.$el.querySelector('.js-source-branch').innerHTML, '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>'); );
}); });
it('renders clipboard button', () => { it('renders clipboard button', () => {
...@@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => { ...@@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => {
}); });
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 12, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 12,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', sourceBranch: 'mr-widget-refactor',
sourceBranchRemoved: false, sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
targetBranchPath: 'foo/bar/commits-path', sourceBranchRemoved: false,
targetBranchTreePath: 'foo/bar/tree/path', targetBranchPath: 'foo/bar/commits-path',
targetBranch: 'master', targetBranchTreePath: 'foo/bar/tree/path',
isOpen: true, targetBranch: 'master',
emailPatchesPath: '/mr/email-patches', isOpen: true,
plainDiffPath: '/mr/plainDiffPath', emailPatchesPath: '/mr/email-patches',
} }); plainDiffPath: '/mr/plainDiffPath',
statusPath: 'abc',
},
});
}); });
it('renders checkout branch button with modal trigger', () => { it('renders checkout branch button with modal trigger', () => {
...@@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => { ...@@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => {
expect(button.getAttribute('data-toggle')).toEqual('modal'); expect(button.getAttribute('data-toggle')).toEqual('modal');
}); });
it('renders web ide button', () => {
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Web IDE');
expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
});
it('renders download dropdown with links', () => { it('renders download dropdown with links', () => {
expect( expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
vm.$el.querySelector('.js-download-email-patches').textContent.trim(), 'Email patches',
).toEqual('Email patches'); );
expect( expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
vm.$el.querySelector('.js-download-email-patches').getAttribute('href'), '/mr/email-patches',
).toEqual('/mr/email-patches'); );
expect( expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
vm.$el.querySelector('.js-download-plain-diff').textContent.trim(), 'Plain diff',
).toEqual('Plain diff'); );
expect( expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'), '/mr/plainDiffPath',
).toEqual('/mr/plainDiffPath'); );
}); });
}); });
describe('with a closed merge request', () => { describe('with a closed merge request', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 12, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 12,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', sourceBranch: 'mr-widget-refactor',
sourceBranchRemoved: false, sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
targetBranchPath: 'foo/bar/commits-path', sourceBranchRemoved: false,
targetBranchTreePath: 'foo/bar/tree/path', targetBranchPath: 'foo/bar/commits-path',
targetBranch: 'master', targetBranchTreePath: 'foo/bar/tree/path',
isOpen: false, targetBranch: 'master',
emailPatchesPath: '/mr/email-patches', isOpen: false,
plainDiffPath: '/mr/plainDiffPath', emailPatchesPath: '/mr/email-patches',
} }); plainDiffPath: '/mr/plainDiffPath',
statusPath: 'abc',
},
});
}); });
it('does not render checkout branch button with modal trigger', () => { it('does not render checkout branch button with modal trigger', () => {
...@@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => { ...@@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => {
}); });
it('does not render download dropdown with links', () => { it('does not render download dropdown with links', () => {
expect( expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
vm.$el.querySelector('.js-download-email-patches'),
).toEqual(null);
expect( expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
vm.$el.querySelector('.js-download-plain-diff'),
).toEqual(null);
}); });
}); });
describe('without diverged commits', () => { describe('without diverged commits', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 0, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 0,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', sourceBranch: 'mr-widget-refactor',
sourceBranchRemoved: false, sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
targetBranchPath: 'foo/bar/commits-path', sourceBranchRemoved: false,
targetBranchTreePath: 'foo/bar/tree/path', targetBranchPath: 'foo/bar/commits-path',
targetBranch: 'master', targetBranchTreePath: 'foo/bar/tree/path',
isOpen: true, targetBranch: 'master',
emailPatchesPath: '/mr/email-patches', isOpen: true,
plainDiffPath: '/mr/plainDiffPath', emailPatchesPath: '/mr/email-patches',
} }); plainDiffPath: '/mr/plainDiffPath',
statusPath: 'abc',
},
});
}); });
it('does not render diverged commits info', () => { it('does not render diverged commits info', () => {
...@@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => { ...@@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => {
describe('with diverged commits', () => { describe('with diverged commits', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { mr: { vm = mountComponent(Component, {
divergedCommitsCount: 12, mr: {
sourceBranch: 'mr-widget-refactor', divergedCommitsCount: 12,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', sourceBranch: 'mr-widget-refactor',
sourceBranchRemoved: false, sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
targetBranchPath: 'foo/bar/commits-path', sourceBranchRemoved: false,
targetBranchTreePath: 'foo/bar/tree/path', targetBranchPath: 'foo/bar/commits-path',
targetBranch: 'master', targetBranchTreePath: 'foo/bar/tree/path',
isOpen: true, targetBranch: 'master',
emailPatchesPath: '/mr/email-patches', isOpen: true,
plainDiffPath: '/mr/plainDiffPath', emailPatchesPath: '/mr/email-patches',
} }); plainDiffPath: '/mr/plainDiffPath',
statusPath: 'abc',
},
});
}); });
it('renders diverged commits info', () => { it('renders diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)'); expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
'(12 commits behind)',
);
}); });
}); });
}); });
......
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