Commit eaed588b authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'ide-pending-tab' into 'master'

Added pending tabs to IDE

See merge request gitlab-org/gitlab-ce!17936
parents 1f59aa24 259adc34
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import router from '../../ide_router';
export default { export default {
components: { components: {
icon, Icon,
}, },
props: { props: {
file: { file: {
...@@ -22,17 +21,16 @@ ...@@ -22,17 +21,16 @@
}, },
}, },
methods: { methods: {
...mapActions([ ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
'discardFileChanges',
'updateViewer',
]),
openFileInEditor(file) { openFileInEditor(file) {
return this.openPendingTab(file).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff'); this.updateViewer('diff');
}
router.push(`/project${file.url}`); });
}, },
}, },
}; };
</script> </script>
<template> <template>
......
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
v-if="activeFile" v-if="activeFile"
> >
<repo-tabs <repo-tabs
:active-file="activeFile"
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"
:has-changes="hasChanges" :has-changes="hasChanges"
......
...@@ -21,7 +21,8 @@ export default { ...@@ -21,7 +21,8 @@ export default {
}, },
watch: { watch: {
file(oldVal, newVal) { 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(); this.initMonaco();
} }
}, },
...@@ -70,7 +71,7 @@ export default { ...@@ -70,7 +71,7 @@ export default {
}) })
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor') ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve(); : Promise.resolve();
return viewerPromise; return viewerPromise;
......
...@@ -62,11 +62,7 @@ export default { ...@@ -62,11 +62,7 @@ export default {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
const delayPromise = this.file.changed return this.updateDelayViewerUpdated(true).then(() => {
? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
router.push(`/project${this.file.url}`); router.push(`/project${this.file.url}`);
}); });
}, },
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import fileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import ChangedFileIcon from './changed_file_icon.vue';
export default { export default {
components: { components: {
fileStatusIcon, FileStatusIcon,
fileIcon, FileIcon,
icon, Icon,
changedFileIcon, ChangedFileIcon,
}, },
props: { props: {
tab: { tab: {
...@@ -37,11 +37,15 @@ ...@@ -37,11 +37,15 @@
}, },
methods: { methods: {
...mapActions([ ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
'closeFile',
]),
clickFile(tab) { clickFile(tab) {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
this.openPendingTab(tab);
} else {
this.$router.push(`/project${tab.url}`); this.$router.push(`/project${tab.url}`);
}
}, },
mouseOverTab() { mouseOverTab() {
if (this.tab.changed) { if (this.tab.changed) {
...@@ -54,7 +58,7 @@ ...@@ -54,7 +58,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -66,7 +70,7 @@ ...@@ -66,7 +70,7 @@
<button <button
type="button" type="button"
class="multi-file-tab-close" class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab.path)" @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel" :aria-label="closeLabel"
> >
<icon <icon
...@@ -82,7 +86,9 @@ ...@@ -82,7 +86,9 @@
<div <div
class="multi-file-tab" class="multi-file-tab"
:class="{active : tab.active }" :class="{
active: tab.active
}"
:title="tab.url" :title="tab.url"
> >
<file-icon <file-icon
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue'; import EditorMode from './editor_mode_dropdown.vue';
import router from '../ide_router';
export default { export default {
components: { components: {
...@@ -9,6 +10,10 @@ export default { ...@@ -9,6 +10,10 @@ export default {
EditorMode, EditorMode,
}, },
props: { props: {
activeFile: {
type: Object,
required: true,
},
files: { files: {
type: Array, type: Array,
required: true, required: true,
...@@ -38,7 +43,18 @@ export default { ...@@ -38,7 +43,18 @@ export default {
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth; this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
}, },
methods: { 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> </script>
...@@ -60,7 +76,7 @@ export default { ...@@ -60,7 +76,7 @@ export default {
:show-shadow="showShadow" :show-shadow="showShadow"
:has-changes="hasChanges" :has-changes="hasChanges"
:merge-request-id="mergeRequestId" :merge-request-id="mergeRequestId"
@click="updateViewer" @click="openFileViewer"
/> />
</div> </div>
</template> </template>
...@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => { ...@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => {
if (to.params[0]) { if (to.params[0]) {
const path = 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]; const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
const treeEntry = store.state.entries[treeEntryKey];
if (treeEntry) { if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry); store.dispatch('handleTreeEntryAction', treeEntry);
} }
......
...@@ -13,12 +13,12 @@ export default class Model { ...@@ -13,12 +13,12 @@ export default class Model {
(this.originalModel = this.monaco.editor.createModel( (this.originalModel = this.monaco.editor.createModel(
this.file.raw, this.file.raw,
undefined, 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.model = this.monaco.editor.createModel(
this.content, this.content,
undefined, undefined,
new this.monaco.Uri(null, null, this.file.path), new this.monaco.Uri(null, null, this.file.key),
)), )),
); );
if (this.file.mrChange) { if (this.file.mrChange) {
...@@ -36,7 +36,7 @@ export default class Model { ...@@ -36,7 +36,7 @@ export default class Model {
this.updateContent = this.updateContent.bind(this); this.updateContent = this.updateContent.bind(this);
this.dispose = this.dispose.bind(this); this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
...@@ -53,7 +53,7 @@ export default class Model { ...@@ -53,7 +53,7 @@ export default class Model {
} }
get path() { get path() {
return this.file.path; return this.file.key;
} }
getModel() { getModel() {
...@@ -88,7 +88,7 @@ export default class Model { ...@@ -88,7 +88,7 @@ export default class Model {
this.disposable.dispose(); this.disposable.dispose();
this.events.clear(); 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); eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
} }
...@@ -9,17 +9,17 @@ export default class ModelManager { ...@@ -9,17 +9,17 @@ export default class ModelManager {
this.models = new Map(); this.models = new Map();
} }
hasCachedModel(path) { hasCachedModel(key) {
return this.models.has(path); return this.models.has(key);
} }
getModel(path) { getModel(key) {
return this.models.get(path); return this.models.get(key);
} }
addModel(file) { addModel(file) {
if (this.hasCachedModel(file.path)) { if (this.hasCachedModel(file.key)) {
return this.getModel(file.path); return this.getModel(file.key);
} }
const model = new Model(this.monaco, file); const model = new Model(this.monaco, file);
...@@ -27,7 +27,7 @@ export default class ModelManager { ...@@ -27,7 +27,7 @@ export default class ModelManager {
this.disposable.add(model); this.disposable.add(model);
eventHub.$on( eventHub.$on(
`editor.update.model.dispose.${file.path}`, `editor.update.model.dispose.${file.key}`,
this.removeCachedModel.bind(this, file), this.removeCachedModel.bind(this, file),
); );
...@@ -35,12 +35,9 @@ export default class ModelManager { ...@@ -35,12 +35,9 @@ export default class ModelManager {
} }
removeCachedModel(file) { removeCachedModel(file) {
this.models.delete(file.path); this.models.delete(file.key);
eventHub.$off( eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
`editor.update.model.dispose.${file.path}`,
this.removeCachedModel,
);
} }
dispose() { dispose() {
......
...@@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => { ...@@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
}; };
export const closeAllFiles = ({ state, 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 }) => { export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
......
...@@ -6,24 +6,34 @@ import * as types from '../mutation_types'; ...@@ -6,24 +6,34 @@ import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path); const path = file.path;
const file = state.entries[path]; const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active; const fileWasActive = file.active;
if (file.pending) {
commit(types.REMOVE_PENDING_TAB, file);
} else {
commit(types.TOGGLE_FILE_OPEN, path); commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false }); commit(types.SET_FILE_ACTIVE, { path, active: false });
}
if (state.openFiles.length > 0 && fileWasActive) { if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path]; const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
dispatch('openPendingTab', nextFileToOpen);
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`); router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`); 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) => { export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
...@@ -151,3 +161,23 @@ export const discardFileChanges = ({ state, commit }, path) => { ...@@ -151,3 +161,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw); 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}`);
};
...@@ -49,3 +49,6 @@ export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; ...@@ -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 SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER'; export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
...@@ -5,6 +5,14 @@ export default { ...@@ -5,6 +5,14 @@ export default {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
active, 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) { [types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
...@@ -12,10 +20,14 @@ export default { ...@@ -12,10 +20,14 @@ export default {
}); });
if (state.entries[path].opened) { 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 { } else {
const file = state.entries[path];
Object.assign(state, { 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 { ...@@ -92,4 +104,37 @@ export default {
changed, 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),
});
},
}; };
export const dataStructure = () => ({ export const dataStructure = () => ({
id: '', id: '',
// Key will contain a mixture of ID and path
// it can also contain a prefix `pending-` for files opened in review mode
key: '', key: '',
type: '', type: '',
projectId: '', projectId: '',
......
import Vue from 'vue'; import Vue from 'vue';
import listItem from '~/ide/components/commit_sidebar/list_item.vue'; import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import store from '~/ide/stores';
import { file } from '../../helpers'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => { describe('Multi-file editor commit sidebar list item', () => {
let vm; let vm;
...@@ -13,19 +14,21 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -13,19 +14,21 @@ describe('Multi-file editor commit sidebar list item', () => {
f = file('test-file'); f = file('test-file');
vm = mountComponent(Component, { store.state.entries[f.path] = f;
vm = createComponentWithStore(Component, store, {
file: f, file: f,
}); }).$mount();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
resetStore(store);
}); });
it('renders file path', () => { it('renders file path', () => {
expect( expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
).toBe(f.path);
}); });
it('calls discardFileChanges when clicking discard button', () => { it('calls discardFileChanges when clicking discard button', () => {
...@@ -36,25 +39,32 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -36,25 +39,32 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.discardFileChanges).toHaveBeenCalled(); 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, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push'); spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click(); vm.$el.querySelector('.multi-file-commit-list-path').click();
setTimeout(() => {
expect(vm.openFileInEditor).toHaveBeenCalled(); expect(vm.openFileInEditor).toHaveBeenCalled();
expect(router.push).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, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer'); spyOn(vm, 'updateViewer').and.callThrough();
spyOn(router, 'push'); spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click(); vm.$el.querySelector('.multi-file-commit-list-path').click();
setTimeout(() => {
expect(vm.updateViewer).toHaveBeenCalledWith('diff'); expect(vm.updateViewer).toHaveBeenCalledWith('diff');
done();
});
}); });
describe('computed', () => { describe('computed', () => {
......
...@@ -59,7 +59,7 @@ describe('RepoTab', () => { ...@@ -59,7 +59,7 @@ describe('RepoTab', () => {
vm.$el.querySelector('.multi-file-tab-close').click(); 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 => { it('changes icon on hover', done => {
......
...@@ -17,6 +17,7 @@ describe('RepoTabs', () => { ...@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
files: openedFiles, files: openedFiles,
viewer: 'editor', viewer: 'editor',
hasChanges: false, hasChanges: false,
activeFile: file('activeFile'),
hasMergeRequest: false, hasMergeRequest: false,
}); });
openedFiles[0].active = true; openedFiles[0].active = true;
...@@ -57,6 +58,7 @@ describe('RepoTabs', () => { ...@@ -57,6 +58,7 @@ describe('RepoTabs', () => {
files: [], files: [],
viewer: 'editor', viewer: 'editor',
hasChanges: false, hasChanges: false,
activeFile: file('activeFile'),
hasMergeRequest: false, hasMergeRequest: false,
}, },
'#test-app', '#test-app',
......
...@@ -27,9 +27,10 @@ describe('Multi-file editor library model manager', () => { ...@@ -27,9 +27,10 @@ describe('Multi-file editor library model manager', () => {
}); });
it('caches model by file path', () => { 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', () => { it('adds model into disposable', () => {
...@@ -56,7 +57,7 @@ describe('Multi-file editor library model manager', () => { ...@@ -56,7 +57,7 @@ describe('Multi-file editor library model manager', () => {
instance.addModel(f); instance.addModel(f);
expect(eventHub.$on).toHaveBeenCalledWith( expect(eventHub.$on).toHaveBeenCalledWith(
`editor.update.model.dispose.${f.path}`, `editor.update.model.dispose.${f.key}`,
jasmine.anything(), jasmine.anything(),
); );
}); });
...@@ -68,9 +69,11 @@ describe('Multi-file editor library model manager', () => { ...@@ -68,9 +69,11 @@ describe('Multi-file editor library model manager', () => {
}); });
it('returns true when model exists', () => { 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', () => { ...@@ -103,7 +106,7 @@ describe('Multi-file editor library model manager', () => {
instance.removeCachedModel(f); instance.removeCachedModel(f);
expect(eventHub.$off).toHaveBeenCalledWith( expect(eventHub.$off).toHaveBeenCalledWith(
`editor.update.model.dispose.${f.path}`, `editor.update.model.dispose.${f.key}`,
jasmine.anything(), jasmine.anything(),
); );
}); });
......
...@@ -32,14 +32,14 @@ describe('Multi-file editor library model', () => { ...@@ -32,14 +32,14 @@ describe('Multi-file editor library model', () => {
it('adds eventHub listener', () => { it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith( expect(eventHub.$on).toHaveBeenCalledWith(
`editor.update.model.dispose.${model.file.path}`, `editor.update.model.dispose.${model.file.key}`,
jasmine.anything(), jasmine.anything(),
); );
}); });
describe('path', () => { describe('path', () => {
it('returns file 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', () => { ...@@ -74,7 +74,7 @@ describe('Multi-file editor library model', () => {
model.onChange(() => {}); model.onChange(() => {});
expect(model.events.size).toBe(1); 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 => { it('calls callback on change', done => {
...@@ -115,7 +115,7 @@ describe('Multi-file editor library model', () => { ...@@ -115,7 +115,7 @@ describe('Multi-file editor library model', () => {
model.dispose(); model.dispose();
expect(eventHub.$off).toHaveBeenCalledWith( expect(eventHub.$off).toHaveBeenCalledWith(
`editor.update.model.dispose.${model.file.path}`, `editor.update.model.dispose.${model.file.key}`,
jasmine.anything(), jasmine.anything(),
); );
}); });
......
...@@ -36,9 +36,7 @@ describe('Multi-file editor library decorations controller', () => { ...@@ -36,9 +36,7 @@ describe('Multi-file editor library decorations controller', () => {
}); });
it('returns decorations by model URL', () => { it('returns decorations by model URL', () => {
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' },
]);
const decorations = controller.getAllDecorationsForModel(model); const decorations = controller.getAllDecorationsForModel(model);
...@@ -48,39 +46,29 @@ describe('Multi-file editor library decorations controller', () => { ...@@ -48,39 +46,29 @@ describe('Multi-file editor library decorations controller', () => {
describe('addDecorations', () => { describe('addDecorations', () => {
it('caches decorations in a new map', () => { it('caches decorations in a new map', () => {
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' },
]);
expect(controller.decorations.size).toBe(1); expect(controller.decorations.size).toBe(1);
}); });
it('does not create new cache model', () => { it('does not create new cache model', () => {
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' }, controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
]);
controller.addDecorations(model, 'key', [
{ decoration: 'decorationValue2' },
]);
expect(controller.decorations.size).toBe(1); expect(controller.decorations.size).toBe(1);
}); });
it('caches decorations by model URL', () => { it('caches decorations by model URL', () => {
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' },
]);
expect(controller.decorations.size).toBe(1); 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', () => { it('calls decorate method', () => {
spyOn(controller, 'decorate'); spyOn(controller, 'decorate');
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' },
]);
expect(controller.decorate).toHaveBeenCalled(); expect(controller.decorate).toHaveBeenCalled();
}); });
...@@ -92,10 +80,7 @@ describe('Multi-file editor library decorations controller', () => { ...@@ -92,10 +80,7 @@ describe('Multi-file editor library decorations controller', () => {
controller.decorate(model); controller.decorate(model);
expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith( expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
[],
[],
);
}); });
it('caches decorations', () => { it('caches decorations', () => {
...@@ -111,15 +96,13 @@ describe('Multi-file editor library decorations controller', () => { ...@@ -111,15 +96,13 @@ describe('Multi-file editor library decorations controller', () => {
controller.decorate(model); controller.decorate(model);
expect(controller.editorDecorations.keys().next().value).toBe('path'); expect(controller.editorDecorations.keys().next().value).toBe('path--path');
}); });
}); });
describe('dispose', () => { describe('dispose', () => {
it('clears cached decorations', () => { it('clears cached decorations', () => {
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' },
]);
controller.dispose(); controller.dispose();
...@@ -127,9 +110,7 @@ describe('Multi-file editor library decorations controller', () => { ...@@ -127,9 +110,7 @@ describe('Multi-file editor library decorations controller', () => {
}); });
it('clears cached editorDecorations', () => { it('clears cached editorDecorations', () => {
controller.addDecorations(model, 'key', [ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
{ decoration: 'decorationValue' },
]);
controller.dispose(); controller.dispose();
......
...@@ -131,7 +131,7 @@ describe('Multi-file editor library dirty diff controller', () => { ...@@ -131,7 +131,7 @@ describe('Multi-file editor library dirty diff controller', () => {
it('adds decorations into decorations controller', () => { it('adds decorations into decorations controller', () => {
spyOn(controller.decorationsController, 'addDecorations'); spyOn(controller.decorationsController, 'addDecorations');
controller.decorate({ data: { changes: [], path: 'path' } }); controller.decorate({ data: { changes: [], path: model.path } });
expect( expect(
controller.decorationsController.addDecorations, controller.decorationsController.addDecorations,
...@@ -145,7 +145,7 @@ describe('Multi-file editor library dirty diff controller', () => { ...@@ -145,7 +145,7 @@ describe('Multi-file editor library dirty diff controller', () => {
); );
controller.decorate({ controller.decorate({
data: { changes: computeDiff('123', '1234'), path: 'path' }, data: { changes: computeDiff('123', '1234'), path: model.path },
}); });
expect(spy).toHaveBeenCalledWith( expect(spy).toHaveBeenCalledWith(
......
...@@ -29,7 +29,7 @@ describe('IDE store file actions', () => { ...@@ -29,7 +29,7 @@ describe('IDE store file actions', () => {
it('closes open files', done => { it('closes open files', done => {
store store
.dispatch('closeFile', localFile.path) .dispatch('closeFile', localFile)
.then(() => { .then(() => {
expect(localFile.opened).toBeFalsy(); expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy(); expect(localFile.active).toBeFalsy();
...@@ -44,7 +44,7 @@ describe('IDE store file actions', () => { ...@@ -44,7 +44,7 @@ describe('IDE store file actions', () => {
store.state.changedFiles.push(localFile); store.state.changedFiles.push(localFile);
store store
.dispatch('closeFile', localFile.path) .dispatch('closeFile', localFile)
.then(Vue.nextTick) .then(Vue.nextTick)
.then(() => { .then(() => {
expect(store.state.openFiles.length).toBe(0); expect(store.state.openFiles.length).toBe(0);
...@@ -65,7 +65,7 @@ describe('IDE store file actions', () => { ...@@ -65,7 +65,7 @@ describe('IDE store file actions', () => {
store.state.entries[f.path] = f; store.state.entries[f.path] = f;
store store
.dispatch('closeFile', localFile.path) .dispatch('closeFile', localFile)
.then(Vue.nextTick) .then(Vue.nextTick)
.then(() => { .then(() => {
expect(router.push).toHaveBeenCalledWith(`/project${f.url}`); expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
...@@ -74,6 +74,22 @@ describe('IDE store file actions', () => { ...@@ -74,6 +74,22 @@ describe('IDE store file actions', () => {
}) })
.catch(done.fail); .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', () => { describe('setFileActive', () => {
...@@ -445,4 +461,113 @@ describe('IDE store file actions', () => { ...@@ -445,4 +461,113 @@ describe('IDE store file actions', () => {
.catch(done.fail); .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);
});
});
}); });
...@@ -22,6 +22,21 @@ describe('IDE store file mutations', () => { ...@@ -22,6 +22,21 @@ describe('IDE store file mutations', () => {
expect(localFile.active).toBeTruthy(); 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', () => { describe('TOGGLE_FILE_OPEN', () => {
...@@ -178,4 +193,69 @@ describe('IDE store file mutations', () => { ...@@ -178,4 +193,69 @@ describe('IDE store file mutations', () => {
expect(localFile.changed).toBeTruthy(); 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);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment