summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-04-01 15:24:15 +0000
committerPhil Hughes <me@iamphill.com>2018-04-01 15:24:15 +0000
commit6b89ab116110f177ecb809adf429fe05f76e97f5 (patch)
tree97f179d68eaa79b040ed4a56f4e87144754ff1a7
parentd2ad0bcc0254524c71c7422935f53c267b5b6923 (diff)
parent9d958af5a2a8c667509d6fce9729a1c5997e229b (diff)
downloadgitlab-ce-6b89ab116110f177ecb809adf429fe05f76e97f5.tar.gz
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
-rw-r--r--app/assets/javascripts/api.js146
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue34
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide.vue85
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue29
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue21
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue71
-rw-r--r--app/assets/javascripts/ide/ide_router.js60
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js28
-rw-r--r--app/assets/javascripts/ide/lib/editor.js20
-rw-r--r--app/assets/javascripts/ide/services/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions.js11
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js73
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js84
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js98
-rw-r--r--app/assets/javascripts/ide/stores/getters.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutations/merge_request.js33
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue97
-rw-r--r--app/assets/stylesheets/pages/repo.scss1
-rw-r--r--spec/javascripts/api_spec.js183
-rw-r--r--spec/javascripts/ide/components/changed_file_icon_spec.js3
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js58
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js2
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js14
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js25
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js43
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js110
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js12
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js22
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js26
-rw-r--r--spec/javascripts/ide/stores/mutations/merge_request_spec.js65
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js244
41 files changed, 1380 insertions, 573 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f..8ad3d18b302 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
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',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
- const url = Api.buildUrl(Api.groupPath)
- .replace(':id', groupId);
- return axios.get(url)
- .then(({ data }) => {
- callback(data);
+ const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+ return axios.get(url).then(({ data }) => {
+ callback(data);
- return data;
- });
+ return data;
+ });
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
- return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
+ })
.then(({ data }) => {
callback(data);
@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
- return axios.get(url, {
- params: Object.assign(defaults, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(defaults, options),
+ })
.then(({ data }) => {
callback(data);
@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
- const url = Api.buildUrl(Api.projectPath)
- .replace(':id', encodeURIComponent(projectPath));
+ const url = Api.buildUrl(Api.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);
},
@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
- return axios.post(url, {
- label: data,
- })
+ return axios
+ .post(url, {
+ label: data,
+ })
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
- const url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', groupId);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const url = Api.buildUrl(Api.commitPath)
- .replace(':id', encodeURIComponent(id));
+ const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
- .replace(':branch', branch);
+ .replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
- const url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return axios.get(url, {
- params: data,
- })
+ const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ return axios
+ .get(url, {
+ params: data,
+ })
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
- const url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
- const url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
- return axios.get(url)
+ return axios
+ .get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
});
},
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 0c54c992e51..037e3efb4ce 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,25 +1,25 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
+import icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- props: {
- file: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ changedIcon() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
},
- computed: {
- changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- changedIconClass() {
- return `multi-${this.changedIcon}`;
- },
+ changedIconClass() {
+ return `multi-${this.changedIcon}`;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0..0c44a755f56 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ hasChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- viewer: {
- type: String,
- required: true,
- },
- showShadow: {
- type: Boolean,
- required: true,
- },
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- changeMode(mode) {
- this.$emit('click', mode);
- },
+ viewer: {
+ type: String,
+ 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>
<template>
@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === 'mrdiff' && mergeRequestId">
+ {{ mergeReviewLine }}
+ </template>
+ <template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<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>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 015e750525a..5e44af01241 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,51 +1,51 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import repoFileButtons from './repo_file_buttons.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoEditor from './repo_editor.vue';
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoEditor from './repo_editor.vue';
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- repoFileButtons,
- ideStatusBar,
- repoEditor,
+export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ },
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
},
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer']),
- ...mapGetters(['activeFile', 'hasChanges']),
+ committedStateSvgPath: {
+ type: String,
+ required: true,
},
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ },
+ computed: {
+ ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+ ...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, {
- returnValue,
- });
- return returnValue;
- };
- },
- };
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
</script>
<template>
@@ -63,6 +63,7 @@
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 00000000000..8a440902dfc
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<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>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index e73d1ce839f..b6f8f8a1c99 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -13,12 +13,8 @@ export default {
},
},
computed: {
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'viewer',
- 'delayViewerUpdated',
- ]),
+ ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+ ...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
@@ -68,9 +64,14 @@ export default {
this.editor.clearEditor();
- this.getRawFileData(this.file)
+ this.getRawFileData({
+ path: this.file.path,
+ baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ })
.then(() => {
- const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
+ const viewerPromise = this.delayViewerUpdated
+ ? this.updateViewer('editor')
+ : Promise.resolve();
return viewerPromise;
})
@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
- .catch((err) => {
+ .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
@@ -101,9 +102,13 @@ export default {
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;
if (file.active) {
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 297b9c2628f..1935ee1a4bb 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
+ mrFileIcon,
},
props: {
file: {
@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (
- this.isTree &&
- this.$router.currentRoute.path === `/project${this.file.url}`
- ) {
+ if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
@@ -102,11 +101,15 @@ export default {
:file="file"
/>
</span>
- <changed-file-icon
- :file="file"
- v-if="file.changed || file.tempFile"
- class="prepend-top-5 pull-right"
- />
+ <span class="pull-right">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <changed-file-icon
+ :file="file"
+ v-if="file.changed || file.tempFile"
+ />
+ </span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d5..97589e116c5 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ 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>
<template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 8ea64ddf84a..a44e418b2eb 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,42 +1,46 @@
<script>
- import { mapActions } from 'vuex';
- import RepoTab from './repo_tab.vue';
- import EditorMode from './editor_mode_dropdown.vue';
+import { mapActions } from 'vuex';
+import RepoTab from './repo_tab.vue';
+import EditorMode from './editor_mode_dropdown.vue';
- export default {
- components: {
- RepoTab,
- EditorMode,
+export default {
+ components: {
+ RepoTab,
+ EditorMode,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- props: {
- files: {
- type: Array,
- required: true,
- },
- viewer: {
- type: String,
- required: true,
- },
- hasChanges: {
- type: Boolean,
- required: true,
- },
+ viewer: {
+ type: String,
+ required: true,
},
- data() {
- return {
- showShadow: false,
- };
+ hasChanges: {
+ type: Boolean,
+ required: true,
},
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow =
- this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
- methods: {
- ...mapActions(['updateViewer']),
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- };
+ },
+ data() {
+ return {
+ showShadow: false,
+ };
+ },
+ updated() {
+ if (!this.$refs.tabsScroller) return;
+
+ this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
</script>
<template>
@@ -55,6 +59,7 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
+ :merge-request-id="mergeRequestId"
@click="updateViewer"
/>
</div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index db89c1d44db..be2c12c0487 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
- path: 'mr/:mrid',
+ path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
if (to.params[0]) {
const path =
- to.params[0].slice(-1) === '/'
- ? to.params[0].slice(0, -1)
- : to.params[0];
+ to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
const treeEntry = store.state.entries[path];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
);
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 => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 73cd684351c..d372c2aaad8 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -21,6 +21,15 @@ export default class Model {
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();
@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
- eventHub.$on(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel;
}
+ getBaseModel() {
+ return this.baseModel;
+ }
+
setValue(value) {
this.getModel().setValue(value);
}
@@ -78,13 +88,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
- eventHub.$off(
- `editor.update.model.dispose.${this.file.path}`,
- this.dispose,
- );
- eventHub.$off(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+ eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1..6b4ba30e086 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -109,11 +109,19 @@ export default class Editor {
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() {
- this.monaco.editor.defineTheme(
- gitlabTheme.themeName,
- gitlabTheme.monacoTheme,
- );
+ this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
- this.disposable.add(
- this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
- );
+ this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
}
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843..a12e637616a 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
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());
},
getProjectData(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) {
return Api.branchSingle(projectId, currentBranchId);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7e920aa9f30..0a74f4f8925 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
-export const setInitialData = ({ commit }, data) =>
- commit(types.SET_INITIAL_DATA, data);
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
@@ -43,14 +42,11 @@ export const createTempEntry = (
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
- const fullName =
- name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+ const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
- `The name "${name
- .split('/')
- .pop()}" is already taken in this directory.`,
+ `The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
document,
null,
@@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index ddc4b757bf9..c21c1a3f5d4 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
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 });
-
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file.path);
- dispatch('setFileActive', file.path);
+ commit(types.TOGGLE_FILE_OPEN, path);
+ if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash(
- 'Error loading file data. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
-export const getRawFileData = ({ commit, dispatch }, file) =>
- service
- .getRawFileData(file)
- .then(raw => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() =>
- flash(
- 'Error loading file content. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- ),
- );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+ const file = state.entries[path];
+ return new Promise((resolve, reject) => {
+ service
+ .getRawFileData(file)
+ .then(raw => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (file.mrChange && file.mrChange.new_file === false) {
+ service
+ .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 }) => {
const file = state.entries[path];
@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
}
};
-export const setEditorPosition = (
- { getters, commit },
- { editorRow, editorColumn },
-) => {
+export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 00000000000..da73034fd7d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+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);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a0325..6536be04f0a 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
-import {
- findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
- dispatch('getFileData', row);
+ dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
+ service
+ .getTreeLastCommit(tree.lastCommitPath)
+ .then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
- .then((data) => {
- data.forEach((lastCommit) => {
+ .then(data => {
+ data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
-export const getFiles = (
- { state, commit, dispatch },
- { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
- const selectedProject = state.projects[projectId];
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
- service
- .getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then((data) => {
- const worker = new FilesDecoratorWorker();
- worker.addEventListener('message', (e) => {
- const { entries, treeList } = e.data;
- const selectedTree = state.trees[`${projectId}/${branchId}`];
-
- commit(types.SET_ENTRIES, entries);
- commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
- commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
- worker.terminate();
-
- resolve();
- });
-
- worker.postMessage({
- data,
- projectId,
- branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+ new Promise((resolve, reject) => {
+ if (!state.trees[`${projectId}/${branchId}`]) {
+ const selectedProject = state.projects[projectId];
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+ service
+ .getFiles(selectedProject.web_url, branchId)
+ .then(res => res.json())
+ .then(data => {
+ const worker = new FilesDecoratorWorker();
+ worker.addEventListener('message', e => {
+ const { entries, treeList } = e.data;
+ const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+ commit(types.SET_ENTRIES, entries);
+ commit(types.SET_DIRECTORY_DATA, {
+ treePath: `${projectId}/${branchId}`,
+ data: treeList,
+ });
+ commit(types.TOGGLE_LOADING, {
+ entry: selectedTree,
+ forceValue: false,
+ });
+
+ worker.terminate();
+
+ resolve();
+ });
+
+ worker.postMessage({
+ data,
+ projectId,
+ branchId,
+ });
+ })
+ .catch(e => {
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ reject(e);
});
- })
- .catch((e) => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
- reject(e);
- });
- } else {
- resolve();
- }
-});
-
+ } else {
+ resolve();
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df..a77cdbc13c8 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
- state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
-export const modifiedFiles = state =>
- state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
@@ -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
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e28f190897c..e06be0a3fe9 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
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
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
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 SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
@@ -39,5 +46,6 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
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_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c..5e5eb831662 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
import * as types from './mutation_types';
import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
- loading:
- forceValue !== undefined
- ? forceValue
- : !state.entries[entry.path].loading,
+ loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
@@ -83,9 +81,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(
- data.treeList,
- ),
+ tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
@@ -100,6 +96,7 @@ export default {
});
},
...projectMutations,
+ ...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 2500f13db7c..692fe39b38e 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -28,6 +28,8 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
+ raw: null,
+ baseRaw: null,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -35,6 +37,11 @@ export default {
raw,
});
},
+ [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+ Object.assign(state.entries[file.path], {
+ baseRaw,
+ });
+ },
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
@@ -59,6 +66,11 @@ export default {
editorColumn,
});
},
+ [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ Object.assign(state.entries[file.path], {
+ mrChange,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 00000000000..334819fe702
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+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,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a919..284b39a2c72 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
+ mergeRequests: {},
active: true,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c..e5cc8814000 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
currentProjectId: '',
currentBranchId: '',
+ currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e..3389eeeaa2e 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '',
});
-export const decorateData = (entity) => {
+export const decorateData = entity => {
const {
id,
projectId,
@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false,
file_lock,
-
} = entity;
return {
@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
base64,
file_lock,
-
};
};
-export const findEntry = (tree, type, name, prop = 'name') => tree.find(
- f => f.type === type && f[prop] === name,
-);
+export const findEntry = (tree, type, name, 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 setPageTitle = (title) => {
+export const setPageTitle = title => {
document.title = title;
};
@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
-export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
- tree: entity.tree.length ? sortTree(entity.tree) : [],
-})).sort(sortTreesByTypeAndName);
+export const sortTree = sortedTree =>
+ sortedTree
+ .map(entity =>
+ Object.assign(entity, {
+ tree: entity.tree.length ? sortTree(entity.tree) : [],
+ }),
+ )
+ .sort(sortTreesByTypeAndName);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771f..dd17544b656 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
- params.forEach((param) => {
+ params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(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;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628..18ee4c62bf1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
- import { n__ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
- export default {
- name: 'MRWidgetHeader',
- directives: {
- tooltip,
+export default {
+ name: 'MRWidgetHeader',
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ clipboardButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
},
- components: {
- icon,
- clipboardButton,
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
},
- props: {
- mr: {
- type: Object,
- required: true,
- },
+ commitsText() {
+ return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- commitsText() {
- return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
- },
- 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);
- },
+ 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}\``,
+ });
},
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
+ isSourceBranchLong() {
+ return this.isBranchTitleLong(this.mr.sourceBranch);
},
- };
+ isTargetBranchLong() {
+ return this.isBranchTitleLong(this.mr.targetBranch);
+ },
+ webIdePath() {
+ return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+};
</script>
<template>
<div class="mr-source-target">
@@ -96,6 +100,13 @@
</div>
<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
data-target="#modal_merge_info"
data-toggle="modal"
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 34340853165..96e781146a7 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -53,6 +53,7 @@
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
+ max-width: inherit;
svg {
vertical-align: middle;
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 5477581c1b9..3d7ccf432be 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -35,14 +35,14 @@ describe('Api', () => {
});
describe('group', () => {
- it('fetches a group', (done) => {
+ it('fetches a group', done => {
const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
mock.onGet(expectedUrl).reply(200, {
name: 'test',
});
- Api.group(groupId, (response) => {
+ Api.group(groupId, response => {
expect(response.name).toBe('test');
done();
});
@@ -50,15 +50,17 @@ describe('Api', () => {
});
describe('groups', () => {
- it('fetches groups', (done) => {
+ it('fetches groups', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.groups(query, options, (response) => {
+ Api.groups(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -67,14 +69,16 @@ describe('Api', () => {
});
describe('namespaces', () => {
- it('fetches namespaces', (done) => {
+ it('fetches namespaces', done => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.namespaces(query, (response) => {
+ Api.namespaces(query, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -83,31 +87,35 @@ describe('Api', () => {
});
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 options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.projects(query, options, (response) => {
+ Api.projects(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
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 options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.projects(query, options, (response) => {
+ Api.projects(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -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', () => {
- it('creates a new label', (done) => {
+ it('creates a new label', done => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
@@ -124,36 +189,42 @@ describe('Api', () => {
const expectedData = {
label: labelData,
};
- mock.onPost(expectedUrl).reply((config) => {
+ mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData));
- return [200, {
- name: 'test',
- }];
+ return [
+ 200,
+ {
+ name: 'test',
+ },
+ ];
});
- Api.newLabel(namespace, project, labelData, (response) => {
+ Api.newLabel(namespace, project, labelData, response => {
expect(response.name).toBe('test');
done();
});
});
- it('creates a group label', (done) => {
+ it('creates a group label', done => {
const namespace = 'group/subgroup';
const labelData = { some: 'data' };
const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
const expectedData = {
label: labelData,
};
- mock.onPost(expectedUrl).reply((config) => {
+ mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData));
- return [200, {
- name: 'test',
- }];
+ return [
+ 200,
+ {
+ name: 'test',
+ },
+ ];
});
- Api.newLabel(namespace, undefined, labelData, (response) => {
+ Api.newLabel(namespace, undefined, labelData, response => {
expect(response.name).toBe('test');
done();
});
@@ -161,15 +232,17 @@ describe('Api', () => {
});
describe('groupProjects', () => {
- it('fetches group projects', (done) => {
+ it('fetches group projects', done => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.groupProjects(groupId, query, (response) => {
+ Api.groupProjects(groupId, query, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -178,13 +251,13 @@ describe('Api', () => {
});
describe('licenseText', () => {
- it('fetches a license text', (done) => {
+ it('fetches a license text', done => {
const licenseKey = "driver's license";
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.licenseText(licenseKey, data, (response) => {
+ Api.licenseText(licenseKey, data, response => {
expect(response).toBe('test');
done();
});
@@ -192,12 +265,12 @@ describe('Api', () => {
});
describe('gitignoreText', () => {
- it('fetches a gitignore text', (done) => {
+ it('fetches a gitignore text', done => {
const gitignoreKey = 'ignore git';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.gitignoreText(gitignoreKey, (response) => {
+ Api.gitignoreText(gitignoreKey, response => {
expect(response).toBe('test');
done();
});
@@ -205,12 +278,12 @@ describe('Api', () => {
});
describe('gitlabCiYml', () => {
- it('fetches a .gitlab-ci.yml', (done) => {
+ it('fetches a .gitlab-ci.yml', done => {
const gitlabCiYmlKey = 'Y CI ML';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.gitlabCiYml(gitlabCiYmlKey, (response) => {
+ Api.gitlabCiYml(gitlabCiYmlKey, response => {
expect(response).toBe('test');
done();
});
@@ -218,12 +291,12 @@ describe('Api', () => {
});
describe('dockerfileYml', () => {
- it('fetches a Dockerfile', (done) => {
+ it('fetches a Dockerfile', done => {
const dockerfileYmlKey = 'a giant whale';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.dockerfileYml(dockerfileYmlKey, (response) => {
+ Api.dockerfileYml(dockerfileYmlKey, response => {
expect(response).toBe('test');
done();
});
@@ -231,12 +304,14 @@ describe('Api', () => {
});
describe('issueTemplate', () => {
- it('fetches an issue template', (done) => {
+ it('fetches an issue template', done => {
const namespace = 'some namespace';
const project = 'some project';
const templateKey = ' template #%?.key ';
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');
Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
@@ -247,13 +322,15 @@ describe('Api', () => {
});
describe('users', () => {
- it('fetches users', (done) => {
+ it('fetches users', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
Api.users(query, options)
.then(({ data }) => {
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
index 987aea7befc..541864e912e 100644
--- a/spec/javascripts/ide/components/changed_file_icon_spec.js
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => {
vm = createComponent(component, {
file: {
tempFile: false,
+ changed: true,
},
});
});
@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => {
});
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');
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index ae657e8c881..9d3fa1280f4 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -89,6 +89,20 @@ describe('RepoEditor', () => {
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', () => {
@@ -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);
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index ceb0416aff8..73ea7960485 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
files: openedFiles,
viewer: 'editor',
hasChanges: false,
+ hasMergeRequest: false,
});
openedFiles[0].active = true;
@@ -56,6 +57,7 @@ describe('RepoTabs', () => {
files: [],
viewer: 'editor',
hasChanges: false,
+ hasMergeRequest: false,
},
'#test-app',
);
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index adc6a93c06b..7cd990adb53 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => {
spyOn(eventHub, '$on').and.callThrough();
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();
});
@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => {
model.dispose();
});
- it('creates original model & new model', () => {
+ it('creates original model & base model & new model', () => {
expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull();
+ expect(model.baseModel).not.toBeNull();
});
it('adds eventHub listener', () => {
@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => {
});
});
+ describe('getBaseModel', () => {
+ it('returns base model', () => {
+ expect(model.getBaseModel()).toBe(model.baseModel);
+ });
+ });
+
describe('setValue', () => {
it('updates models value', () => {
model.setValue('testing 123');
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 2ccd87de1a7..ec56ebc0341 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -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', () => {
it('resets the editor model', () => {
instance.createInstance(document.createElement('div'));
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 5b7c8365641..2f4516377cf 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -5,7 +5,7 @@ import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
-describe('Multi-file store file actions', () => {
+describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(router, 'push');
});
@@ -189,7 +189,7 @@ describe('Multi-file store file actions', () => {
it('calls the service', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
@@ -200,7 +200,7 @@ describe('Multi-file store file actions', () => {
it('sets the file data', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(localFile.blamePath).toBe('blame_path');
@@ -211,7 +211,7 @@ describe('Multi-file store file actions', () => {
it('sets document title', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(document.title).toBe('testing getFileData');
@@ -222,7 +222,7 @@ describe('Multi-file store file actions', () => {
it('sets the file as active', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(localFile.active).toBeTruthy();
@@ -231,9 +231,20 @@ describe('Multi-file store file actions', () => {
.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 => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(localFile.name);
@@ -256,7 +267,7 @@ describe('Multi-file store file actions', () => {
it('calls getRawFileData service method', done => {
store
- .dispatch('getRawFileData', tmpFile)
+ .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
@@ -267,7 +278,7 @@ describe('Multi-file store file actions', () => {
it('updates file raw data', done => {
store
- .dispatch('getRawFileData', tmpFile)
+ .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(tmpFile.raw).toBe('raw');
@@ -275,6 +286,22 @@ describe('Multi-file store file actions', () => {
})
.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', () => {
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
new file mode 100644
index 00000000000..b4ec4a0b173
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -0,0 +1,110 @@
+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);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 381f038067b..e0ef57a3966 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => {
expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
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].name).toBe(
- 'fileinsubfolder.js',
- );
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
done();
})
@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getLastCommitData', projectTree)
.then(() => {
- expect(service.getTreeLastCommit).toHaveBeenCalledWith(
- 'lastcommitpath',
- );
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
done();
})
@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => {
.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
- expect(projectTree.tree[0].lastCommit.message).not.toBe(
- 'commit message',
- );
+ expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
done();
})
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index a613f3a21cc..33733b97dff 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters';
import state from '~/ide/stores/state';
import { file } from '../helpers';
-describe('Multi-file store getters', () => {
+describe('IDE store getters', () => {
let localState;
beforeEach(() => {
@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => {
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();
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 131380248e8..8fec94e882a 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state';
import { file } from '../../helpers';
-describe('Multi-file store file mutations', () => {
+describe('IDE store file mutations', () => {
let localState;
let localFile;
@@ -62,6 +62,8 @@ describe('Multi-file store file mutations', () => {
expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy();
expect(localFile.renderError).toBe('render_error');
+ expect(localFile.raw).toBeNull();
+ expect(localFile.baseRaw).toBeNull();
});
});
@@ -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', () => {
beforeEach(() => {
localFile.raw = 'test';
@@ -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', () => {
beforeEach(() => {
localFile.content = 'test';
diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
new file mode 100644
index 00000000000..f724bf464f5
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
@@ -0,0 +1,65 @@
+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);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 235c33fac0d..9b9c9656979 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => {
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.shouldShowCommitsBehindText).toEqual(true);
});
it('returns false where there are no divergedComits count', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 0,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.shouldShowCommitsBehindText).toEqual(false);
});
});
describe('commitsText', () => {
it('returns singular when there is one commit', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 1,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 1,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.commitsText).toEqual('1 commit behind');
});
it('returns plural when there is more than one commit', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 2,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 2,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.commitsText).toEqual('2 commits behind');
});
@@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => {
describe('template', () => {
describe('common elements', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('renders source branch link', () => {
- expect(
- vm.$el.querySelector('.js-source-branch').innerHTML,
- ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
+ expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
+ '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ );
});
it('renders clipboard button', () => {
@@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => {
});
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('renders checkout branch button with modal trigger', () => {
@@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => {
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', () => {
- expect(
- vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
- ).toEqual('Email patches');
+ expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
+ 'Email patches',
+ );
- expect(
- vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
- ).toEqual('/mr/email-patches');
+ expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
+ '/mr/email-patches',
+ );
- expect(
- vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
- ).toEqual('Plain diff');
+ expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
+ 'Plain diff',
+ );
- expect(
- vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
- ).toEqual('/mr/plainDiffPath');
+ expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
+ '/mr/plainDiffPath',
+ );
});
});
describe('with a closed merge request', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: false,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: false,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('does not render checkout branch button with modal trigger', () => {
@@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => {
});
it('does not render download dropdown with links', () => {
- expect(
- vm.$el.querySelector('.js-download-email-patches'),
- ).toEqual(null);
+ expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
- expect(
- vm.$el.querySelector('.js-download-plain-diff'),
- ).toEqual(null);
+ expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
});
});
describe('without diverged commits', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 0,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('does not render diverged commits info', () => {
@@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => {
describe('with diverged commits', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
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)',
+ );
});
});
});