summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2018-04-04 10:52:05 +0000
committerTim Zallmann <tzallmann@gitlab.com>2018-04-04 10:52:05 +0000
commiteaed588bf228c833cb666a61bc7d25cf21d5f94b (patch)
treebbe0ff990fec1f5d970b38ce657b64fec2e11cfa
parent1f59aa242279acb1b6feaefd9a868a2353520f02 (diff)
parent259adc34155c2a23dfa482cc1d2664e80200dcc8 (diff)
downloadgitlab-ce-eaed588bf228c833cb666a61bc7d25cf21d5f94b.tar.gz
Merge branch 'ide-pending-tab' into 'master'
Added pending tabs to IDE See merge request gitlab-org/gitlab-ce!17936
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue56
-rw-r--r--app/assets/javascripts/ide/components/ide.vue1
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue106
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue20
-rw-r--r--app/assets/javascripts/ide/ide_router.js6
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js10
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js21
-rw-r--r--app/assets/javascripts/ide/stores/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js48
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js49
-rw-r--r--app/assets/javascripts/ide/stores/utils.js2
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js38
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js2
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js2
-rw-r--r--spec/javascripts/ide/lib/common/model_manager_spec.js15
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js8
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js41
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js4
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js131
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js80
23 files changed, 478 insertions, 178 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 18934af004a..560cdd941cd 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,38 +1,36 @@
<script>
- import { mapActions } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import router from '../../ide_router';
+import { mapActions } from 'vuex';
+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: {
+ iconName() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
},
- computed: {
- iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
- },
+ iconClass() {
+ return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
- methods: {
- ...mapActions([
- 'discardFileChanges',
- 'updateViewer',
- ]),
- openFileInEditor(file) {
- this.updateViewer('diff');
-
- router.push(`/project${file.url}`);
- },
+ },
+ methods: {
+ ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
+ openFileInEditor(file) {
+ return this.openPendingTab(file).then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ });
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 5e44af01241..d22869466c9 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -60,6 +60,7 @@ export default {
v-if="activeFile"
>
<repo-tabs
+ :active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index b6f8f8a1c99..b1a16350c19 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -21,7 +21,8 @@ export default {
},
watch: {
file(oldVal, newVal) {
- if (newVal.path !== this.file.path) {
+ // Compare key to allow for files opened in review mode to be cached differently
+ if (newVal.key !== this.file.key) {
this.initMonaco();
}
},
@@ -70,7 +71,7 @@ export default {
})
.then(() => {
const viewerPromise = this.delayViewerUpdated
- ? this.updateViewer('editor')
+ ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 1935ee1a4bb..3b5068d4910 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -62,11 +62,7 @@ export default {
this.toggleTreeOpen(this.file.path);
}
- const delayPromise = this.file.changed
- ? Promise.resolve()
- : this.updateDelayViewerUpdated(true);
-
- return delayPromise.then(() => {
+ return this.updateDelayViewerUpdated(true).then(() => {
router.push(`/project${this.file.url}`);
});
},
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index c337bc813e6..304a73ed1ad 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,60 +1,64 @@
<script>
- import { mapActions } from 'vuex';
+import { mapActions } from 'vuex';
- import fileIcon from '~/vue_shared/components/file_icon.vue';
- import icon from '~/vue_shared/components/icon.vue';
- import fileStatusIcon from './repo_file_status_icon.vue';
- import changedFileIcon from './changed_file_icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
- export default {
- components: {
- fileStatusIcon,
- fileIcon,
- icon,
- changedFileIcon,
+export default {
+ components: {
+ FileStatusIcon,
+ FileIcon,
+ Icon,
+ ChangedFileIcon,
+ },
+ props: {
+ tab: {
+ type: Object,
+ required: true,
},
- props: {
- tab: {
- type: Object,
- required: true,
- },
+ },
+ data() {
+ return {
+ tabMouseOver: false,
+ };
+ },
+ computed: {
+ closeLabel() {
+ if (this.tab.changed || this.tab.tempFile) {
+ return `${this.tab.name} changed`;
+ }
+ return `Close ${this.tab.name}`;
},
- data() {
- return {
- tabMouseOver: false,
- };
- },
- computed: {
- closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
- return `${this.tab.name} changed`;
- }
- return `Close ${this.tab.name}`;
- },
- showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
- },
+ showChangedIcon() {
+ return this.tab.changed ? !this.tabMouseOver : false;
},
+ },
+
+ methods: {
+ ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
+ clickFile(tab) {
+ this.updateDelayViewerUpdated(true);
- methods: {
- ...mapActions([
- 'closeFile',
- ]),
- clickFile(tab) {
+ if (tab.pending) {
+ this.openPendingTab(tab);
+ } else {
this.$router.push(`/project${tab.url}`);
- },
- mouseOverTab() {
- if (this.tab.changed) {
- this.tabMouseOver = true;
- }
- },
- mouseOutTab() {
- if (this.tab.changed) {
- this.tabMouseOver = false;
- }
- },
+ }
+ },
+ mouseOverTab() {
+ if (this.tab.changed) {
+ this.tabMouseOver = true;
+ }
+ },
+ mouseOutTab() {
+ if (this.tab.changed) {
+ this.tabMouseOver = false;
+ }
},
- };
+ },
+};
</script>
<template>
@@ -66,7 +70,7 @@
<button
type="button"
class="multi-file-tab-close"
- @click.stop.prevent="closeFile(tab.path)"
+ @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
>
<icon
@@ -82,7 +86,9 @@
<div
class="multi-file-tab"
- :class="{active : tab.active }"
+ :class="{
+ active: tab.active
+ }"
:title="tab.url"
>
<file-icon
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index a44e418b2eb..7bd646ba9b0 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -2,6 +2,7 @@
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
+import router from '../ide_router';
export default {
components: {
@@ -9,6 +10,10 @@ export default {
EditorMode,
},
props: {
+ activeFile: {
+ type: Object,
+ required: true,
+ },
files: {
type: Array,
required: true,
@@ -38,7 +43,18 @@ export default {
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
- ...mapActions(['updateViewer']),
+ ...mapActions(['updateViewer', 'removePendingTab']),
+ openFileViewer(viewer) {
+ this.updateViewer(viewer);
+
+ if (this.activeFile.pending) {
+ return this.removePendingTab(this.activeFile).then(() => {
+ router.push(`/project${this.activeFile.url}`);
+ });
+ }
+
+ return null;
+ },
},
};
</script>
@@ -60,7 +76,7 @@ export default {
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
- @click="updateViewer"
+ @click="openFileViewer"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index be2c12c0487..20983666b4a 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => {
if (to.params[0]) {
const path =
to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
- const treeEntry = store.state.entries[path];
+ const treeEntryKey = Object.keys(store.state.entries).find(
+ key => key === path && !store.state.entries[key].pending,
+ );
+ const treeEntry = store.state.entries[treeEntryKey];
+
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index d372c2aaad8..e47adae99ed 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -13,12 +13,12 @@ export default class Model {
(this.originalModel = this.monaco.editor.createModel(
this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.file.path}`),
+ new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
(this.model = this.monaco.editor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.file.path),
+ new this.monaco.Uri(null, null, this.file.key),
)),
);
if (this.file.mrChange) {
@@ -36,7 +36,7 @@ export default class Model {
this.updateContent = this.updateContent.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.key}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
@@ -53,7 +53,7 @@ export default class Model {
}
get path() {
- return this.file.path;
+ return this.file.key;
}
getModel() {
@@ -88,7 +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.dispose.${this.file.key}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 57d5e59a88b..0e7b563b5d6 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -9,17 +9,17 @@ export default class ModelManager {
this.models = new Map();
}
- hasCachedModel(path) {
- return this.models.has(path);
+ hasCachedModel(key) {
+ return this.models.has(key);
}
- getModel(path) {
- return this.models.get(path);
+ getModel(key) {
+ return this.models.get(key);
}
addModel(file) {
- if (this.hasCachedModel(file.path)) {
- return this.getModel(file.path);
+ if (this.hasCachedModel(file.key)) {
+ return this.getModel(file.key);
}
const model = new Model(this.monaco, file);
@@ -27,7 +27,7 @@ export default class ModelManager {
this.disposable.add(model);
eventHub.$on(
- `editor.update.model.dispose.${file.path}`,
+ `editor.update.model.dispose.${file.key}`,
this.removeCachedModel.bind(this, file),
);
@@ -35,12 +35,9 @@ export default class ModelManager {
}
removeCachedModel(file) {
- this.models.delete(file.path);
+ this.models.delete(file.key);
- eventHub.$off(
- `editor.update.model.dispose.${file.path}`,
- this.removeCachedModel,
- );
+ eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
}
dispose() {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 0a74f4f8925..c6ba679d99c 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
};
export const closeAllFiles = ({ state, dispatch }) => {
- state.openFiles.forEach(file => dispatch('closeFile', file.path));
+ state.openFiles.forEach(file => dispatch('closeFile', file));
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index c21c1a3f5d4..6b034ea1e82 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -6,24 +6,34 @@ import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
-export const closeFile = ({ commit, state, getters, dispatch }, path) => {
- const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
- const file = state.entries[path];
+export const closeFile = ({ commit, state, dispatch }, file) => {
+ const path = file.path;
+ const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
- commit(types.TOGGLE_FILE_OPEN, path);
- commit(types.SET_FILE_ACTIVE, { path, active: false });
+ if (file.pending) {
+ commit(types.REMOVE_PENDING_TAB, file);
+ } else {
+ commit(types.TOGGLE_FILE_OPEN, path);
+ commit(types.SET_FILE_ACTIVE, { path, active: false });
+ }
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
- const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
-
- router.push(`/project${nextFileToOpen.url}`);
+ const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+ if (nextFileToOpen.pending) {
+ dispatch('updateViewer', 'diff');
+ dispatch('openPendingTab', nextFileToOpen);
+ } else {
+ dispatch('updateDelayViewerUpdated', true);
+ router.push(`/project${nextFileToOpen.url}`);
+ }
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
- eventHub.$emit(`editor.update.model.dispose.${file.path}`);
+ eventHub.$emit(`editor.update.model.dispose.${file.key}`);
};
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
@@ -151,3 +161,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
};
+
+export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
+ if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+ return false;
+ }
+
+ commit(types.ADD_PENDING_TAB, { file });
+
+ dispatch('scrollToTab');
+
+ router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
+
+ return true;
+};
+
+export const removePendingTab = ({ commit }, file) => {
+ commit(types.REMOVE_PENDING_TAB, file);
+
+ eventHub.$emit(`editor.update.model.dispose.${file.key}`);
+};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e06be0a3fe9..ee759bff516 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -49,3 +49,6 @@ 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';
+
+export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
+export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 692fe39b38e..926b6f66d78 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -5,6 +5,14 @@ export default {
Object.assign(state.entries[path], {
active,
});
+
+ if (active && !state.entries[path].pending) {
+ Object.assign(state, {
+ openFiles: state.openFiles.map(f =>
+ Object.assign(f, { active: f.pending ? false : f.active }),
+ ),
+ });
+ }
},
[types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], {
@@ -12,10 +20,14 @@ export default {
});
if (state.entries[path].opened) {
- state.openFiles.push(state.entries[path]);
+ Object.assign(state, {
+ openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
+ });
} else {
+ const file = state.entries[path];
+
Object.assign(state, {
- openFiles: state.openFiles.filter(f => f.path !== path),
+ openFiles: state.openFiles.filter(f => f.key !== file.key),
});
}
},
@@ -92,4 +104,37 @@ export default {
changed,
});
},
+ [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
+ const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
+ let openFiles = state.openFiles.map(f =>
+ Object.assign(f, { active: f.path === file.path, opened: false }),
+ );
+
+ if (!pendingTab) {
+ const openFile = openFiles.find(f => f.path === file.path);
+
+ openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
+ if (!f) return acc;
+
+ if (f.path === file.path) {
+ return acc.concat({
+ ...f,
+ active: true,
+ pending: true,
+ opened: true,
+ key: `${keyPrefix}-${f.key}`,
+ });
+ }
+
+ return acc.concat(f);
+ }, []);
+ }
+
+ Object.assign(state, { openFiles });
+ },
+ [types.REMOVE_PENDING_TAB](state, file) {
+ Object.assign(state, {
+ openFiles: state.openFiles.filter(f => f.key !== file.key),
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 3389eeeaa2e..63e4de3b17d 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,5 +1,7 @@
export const dataStructure = () => ({
id: '',
+ // Key will contain a mixture of ID and path
+ // it can also contain a prefix `pending-` for files opened in review mode
key: '',
type: '',
projectId: '',
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 15b66952d99..509434e4300 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { file } from '../../helpers';
+import store from '~/ide/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
let vm;
@@ -13,19 +14,21 @@ describe('Multi-file editor commit sidebar list item', () => {
f = file('test-file');
- vm = mountComponent(Component, {
+ store.state.entries[f.path] = f;
+
+ vm = createComponentWithStore(Component, store, {
file: f,
- });
+ }).$mount();
});
afterEach(() => {
vm.$destroy();
+
+ resetStore(store);
});
it('renders file path', () => {
- expect(
- vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
- ).toBe(f.path);
+ expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
});
it('calls discardFileChanges when clicking discard button', () => {
@@ -36,25 +39,32 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.discardFileChanges).toHaveBeenCalled();
});
- it('opens a closed file in the editor when clicking the file path', () => {
+ it('opens a closed file in the editor when clicking the file path', done => {
spyOn(vm, 'openFileInEditor').and.callThrough();
- spyOn(vm, 'updateViewer');
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
- expect(vm.openFileInEditor).toHaveBeenCalled();
- expect(router.push).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(router.push).toHaveBeenCalled();
+
+ done();
+ });
});
- it('calls updateViewer with diff when clicking file', () => {
+ it('calls updateViewer with diff when clicking file', done => {
spyOn(vm, 'openFileInEditor').and.callThrough();
- spyOn(vm, 'updateViewer');
+ spyOn(vm, 'updateViewer').and.callThrough();
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
- expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ setTimeout(() => {
+ expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+
+ done();
+ });
});
describe('computed', () => {
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index ddb5204e3a7..8cabc6e8935 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -59,7 +59,7 @@ describe('RepoTab', () => {
vm.$el.querySelector('.multi-file-tab-close').click();
- expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
+ expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
});
it('changes icon on hover', done => {
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index 73ea7960485..cb785ba2cd3 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,
+ activeFile: file('activeFile'),
hasMergeRequest: false,
});
openedFiles[0].active = true;
@@ -57,6 +58,7 @@ describe('RepoTabs', () => {
files: [],
viewer: 'editor',
hasChanges: false,
+ activeFile: file('activeFile'),
hasMergeRequest: false,
},
'#test-app',
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
index 4381f6fcfd0..c00d590c580 100644
--- a/spec/javascripts/ide/lib/common/model_manager_spec.js
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -27,9 +27,10 @@ describe('Multi-file editor library model manager', () => {
});
it('caches model by file path', () => {
- instance.addModel(file('path-name'));
+ const f = file('path-name');
+ instance.addModel(f);
- expect(instance.models.keys().next().value).toBe('path-name');
+ expect(instance.models.keys().next().value).toBe(f.key);
});
it('adds model into disposable', () => {
@@ -56,7 +57,7 @@ describe('Multi-file editor library model manager', () => {
instance.addModel(f);
expect(eventHub.$on).toHaveBeenCalledWith(
- `editor.update.model.dispose.${f.path}`,
+ `editor.update.model.dispose.${f.key}`,
jasmine.anything(),
);
});
@@ -68,9 +69,11 @@ describe('Multi-file editor library model manager', () => {
});
it('returns true when model exists', () => {
- instance.addModel(file('path-name'));
+ const f = file('path-name');
+
+ instance.addModel(f);
- expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ expect(instance.hasCachedModel(f.key)).toBeTruthy();
});
});
@@ -103,7 +106,7 @@ describe('Multi-file editor library model manager', () => {
instance.removeCachedModel(f);
expect(eventHub.$off).toHaveBeenCalledWith(
- `editor.update.model.dispose.${f.path}`,
+ `editor.update.model.dispose.${f.key}`,
jasmine.anything(),
);
});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 7cd990adb53..8fc2fccb64c 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -32,14 +32,14 @@ describe('Multi-file editor library model', () => {
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(
- `editor.update.model.dispose.${model.file.path}`,
+ `editor.update.model.dispose.${model.file.key}`,
jasmine.anything(),
);
});
describe('path', () => {
it('returns file path', () => {
- expect(model.path).toBe('path');
+ expect(model.path).toBe(model.file.key);
});
});
@@ -74,7 +74,7 @@ describe('Multi-file editor library model', () => {
model.onChange(() => {});
expect(model.events.size).toBe(1);
- expect(model.events.keys().next().value).toBe('path');
+ expect(model.events.keys().next().value).toBe(model.file.key);
});
it('calls callback on change', done => {
@@ -115,7 +115,7 @@ describe('Multi-file editor library model', () => {
model.dispose();
expect(eventHub.$off).toHaveBeenCalledWith(
- `editor.update.model.dispose.${model.file.path}`,
+ `editor.update.model.dispose.${model.file.key}`,
jasmine.anything(),
);
});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index 092170d086a..aec325e26a9 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -36,9 +36,7 @@ describe('Multi-file editor library decorations controller', () => {
});
it('returns decorations by model URL', () => {
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
const decorations = controller.getAllDecorationsForModel(model);
@@ -48,39 +46,29 @@ describe('Multi-file editor library decorations controller', () => {
describe('addDecorations', () => {
it('caches decorations in a new map', () => {
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorations.size).toBe(1);
});
it('does not create new cache model', () => {
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue2' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
expect(controller.decorations.size).toBe(1);
});
it('caches decorations by model URL', () => {
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorations.size).toBe(1);
- expect(controller.decorations.keys().next().value).toBe('path');
+ expect(controller.decorations.keys().next().value).toBe('path--path');
});
it('calls decorate method', () => {
spyOn(controller, 'decorate');
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorate).toHaveBeenCalled();
});
@@ -92,10 +80,7 @@ describe('Multi-file editor library decorations controller', () => {
controller.decorate(model);
- expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith(
- [],
- [],
- );
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
});
it('caches decorations', () => {
@@ -111,15 +96,13 @@ describe('Multi-file editor library decorations controller', () => {
controller.decorate(model);
- expect(controller.editorDecorations.keys().next().value).toBe('path');
+ expect(controller.editorDecorations.keys().next().value).toBe('path--path');
});
});
describe('dispose', () => {
it('clears cached decorations', () => {
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
controller.dispose();
@@ -127,9 +110,7 @@ describe('Multi-file editor library decorations controller', () => {
});
it('clears cached editorDecorations', () => {
- controller.addDecorations(model, 'key', [
- { decoration: 'decorationValue' },
- ]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
controller.dispose();
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index c8f3e9f4830..ff73240734e 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -131,7 +131,7 @@ describe('Multi-file editor library dirty diff controller', () => {
it('adds decorations into decorations controller', () => {
spyOn(controller.decorationsController, 'addDecorations');
- controller.decorate({ data: { changes: [], path: 'path' } });
+ controller.decorate({ data: { changes: [], path: model.path } });
expect(
controller.decorationsController.addDecorations,
@@ -145,7 +145,7 @@ describe('Multi-file editor library dirty diff controller', () => {
);
controller.decorate({
- data: { changes: computeDiff('123', '1234'), path: 'path' },
+ data: { changes: computeDiff('123', '1234'), path: model.path },
});
expect(spy).toHaveBeenCalledWith(
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 2f4516377cf..479ed7ce49e 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -29,7 +29,7 @@ describe('IDE store file actions', () => {
it('closes open files', done => {
store
- .dispatch('closeFile', localFile.path)
+ .dispatch('closeFile', localFile)
.then(() => {
expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy();
@@ -44,7 +44,7 @@ describe('IDE store file actions', () => {
store.state.changedFiles.push(localFile);
store
- .dispatch('closeFile', localFile.path)
+ .dispatch('closeFile', localFile)
.then(Vue.nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(0);
@@ -65,7 +65,7 @@ describe('IDE store file actions', () => {
store.state.entries[f.path] = f;
store
- .dispatch('closeFile', localFile.path)
+ .dispatch('closeFile', localFile)
.then(Vue.nextTick)
.then(() => {
expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
@@ -74,6 +74,22 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
+
+ it('removes file if it pending', done => {
+ store.state.openFiles.push({
+ ...localFile,
+ pending: true,
+ });
+
+ store
+ .dispatch('closeFile', localFile)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
describe('setFileActive', () => {
@@ -445,4 +461,113 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
});
+
+ describe('openPendingTab', () => {
+ let f;
+
+ beforeEach(() => {
+ f = {
+ ...file(),
+ projectId: '123',
+ };
+
+ store.state.entries[f.path] = f;
+ });
+
+ it('makes file pending in openFiles', done => {
+ store
+ .dispatch('openPendingTab', f)
+ .then(() => {
+ expect(store.state.openFiles[0].pending).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns true when opened', done => {
+ store
+ .dispatch('openPendingTab', f)
+ .then(added => {
+ expect(added).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('pushes router URL when added', done => {
+ store.state.currentBranchId = 'master';
+
+ store
+ .dispatch('openPendingTab', f)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls scrollToTab', done => {
+ const scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+
+ store
+ .dispatch('openPendingTab', f)
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns false when passed in file is active & viewer is diff', done => {
+ f.active = true;
+ store.state.openFiles.push(f);
+ store.state.viewer = 'diff';
+
+ store
+ .dispatch('openPendingTab', f)
+ .then(added => {
+ expect(added).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('removePendingTab', () => {
+ let f;
+
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+
+ f = {
+ ...file('pendingFile'),
+ pending: true,
+ };
+ });
+
+ it('removes pending file from open files', done => {
+ store.state.openFiles.push(f);
+
+ store
+ .dispatch('removePendingTab', f)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits event to dispose model', done => {
+ store
+ .dispatch('removePendingTab', f)
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.${f.key}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 8fec94e882a..88285ee409f 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -22,6 +22,21 @@ describe('IDE store file mutations', () => {
expect(localFile.active).toBeTruthy();
});
+
+ it('sets pending tab as not active', () => {
+ localState.openFiles.push({
+ ...localFile,
+ pending: true,
+ active: true,
+ });
+
+ mutations.SET_FILE_ACTIVE(localState, {
+ path: localFile.path,
+ active: true,
+ });
+
+ expect(localState.openFiles[0].active).toBe(false);
+ });
});
describe('TOGGLE_FILE_OPEN', () => {
@@ -178,4 +193,69 @@ describe('IDE store file mutations', () => {
expect(localFile.changed).toBeTruthy();
});
});
+
+ describe('ADD_PENDING_TAB', () => {
+ beforeEach(() => {
+ const f = {
+ ...file('openFile'),
+ path: 'openFile',
+ active: true,
+ opened: true,
+ };
+
+ localState.entries[f.path] = f;
+ localState.openFiles.push(f);
+ });
+
+ it('adds file into openFiles as pending', () => {
+ mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+ expect(localState.openFiles.length).toBe(2);
+ expect(localState.openFiles[1].pending).toBe(true);
+ expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`);
+ });
+
+ it('updates open file to pending', () => {
+ mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] });
+
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('updates pending open file to active', () => {
+ localState.openFiles.push({
+ ...localFile,
+ pending: true,
+ });
+
+ mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+ expect(localState.openFiles[1].pending).toBe(true);
+ expect(localState.openFiles[1].active).toBe(true);
+ });
+
+ it('sets all openFiles to not active', () => {
+ mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+ expect(localState.openFiles.length).toBe(2);
+
+ localState.openFiles.forEach(f => {
+ if (f.pending) {
+ expect(f.active).toBe(true);
+ } else {
+ expect(f.active).toBe(false);
+ }
+ });
+ });
+ });
+
+ describe('REMOVE_PENDING_TAB', () => {
+ it('removes pending tab from openFiles', () => {
+ localFile.key = 'testing';
+ localState.openFiles.push(localFile);
+
+ mutations.REMOVE_PENDING_TAB(localState, localFile);
+
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
});