Commit f31758d6 authored by Simon Knox's avatar Simon Knox

Merge branch '222507-ide-move-file-editor-props' into 'master'

Step 2 - IDE Create a `editor` module for file editor properties

See merge request gitlab-org/gitlab!44797
parents bf8121f8 fdc71976
...@@ -14,6 +14,7 @@ export default { ...@@ -14,6 +14,7 @@ export default {
}, },
computed: { computed: {
...mapGetters(['activeFile']), ...mapGetters(['activeFile']),
...mapGetters('editor', ['activeFileEditor']),
activeFileEOL() { activeFileEOL() {
return getFileEOL(this.activeFile.content); return getFileEOL(this.activeFile.content);
}, },
...@@ -33,8 +34,10 @@ export default { ...@@ -33,8 +34,10 @@ export default {
</gl-link> </gl-link>
</div> </div>
<div>{{ activeFileEOL }}</div> <div>{{ activeFileEOL }}</div>
<div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div> <div v-if="activeFileIsText">
<div>{{ activeFile.fileLanguage }}</div> {{ activeFileEditor.editorRow }}:{{ activeFileEditor.editorColumn }}
</div>
<div>{{ activeFileEditor.fileLanguage }}</div>
</template> </template>
<terminal-sync-status-safe /> <terminal-sync-status-safe />
</div> </div>
......
...@@ -22,6 +22,7 @@ import Editor from '../lib/editor'; ...@@ -22,6 +22,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue'; import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils'; import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils'; import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser'; import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper'; import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
...@@ -49,6 +50,7 @@ export default { ...@@ -49,6 +50,7 @@ export default {
...mapState('rightPane', { ...mapState('rightPane', {
rightPaneIsOpen: 'isOpen', rightPaneIsOpen: 'isOpen',
}), }),
...mapState('editor', ['fileEditors']),
...mapState([ ...mapState([
'viewer', 'viewer',
'panelResizing', 'panelResizing',
...@@ -67,6 +69,9 @@ export default { ...@@ -67,6 +69,9 @@ export default {
'getJsonSchemaForPath', 'getJsonSchemaForPath',
]), ]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']), ...mapGetters('fileTemplates', ['showFileTemplatesBar']),
fileEditor() {
return getFileEditorOrDefault(this.fileEditors, this.file.path);
},
shouldHideEditor() { shouldHideEditor() {
return this.file && !this.file.loading && !isTextFile(this.file); return this.file && !this.file.loading && !isTextFile(this.file);
}, },
...@@ -80,10 +85,10 @@ export default { ...@@ -80,10 +85,10 @@ export default {
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr; return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
}, },
isEditorViewMode() { isEditorViewMode() {
return this.file.viewMode === FILE_VIEW_MODE_EDITOR; return this.fileEditor.viewMode === FILE_VIEW_MODE_EDITOR;
}, },
isPreviewViewMode() { isPreviewViewMode() {
return this.file.viewMode === FILE_VIEW_MODE_PREVIEW; return this.fileEditor.viewMode === FILE_VIEW_MODE_PREVIEW;
}, },
editTabCSS() { editTabCSS() {
return { return {
...@@ -125,8 +130,7 @@ export default { ...@@ -125,8 +130,7 @@ export default {
this.initEditor(); this.initEditor();
if (this.currentActivityView !== leftSidebarViews.edit.name) { if (this.currentActivityView !== leftSidebarViews.edit.name) {
this.setFileViewMode({ this.updateEditor({
file: this.file,
viewMode: FILE_VIEW_MODE_EDITOR, viewMode: FILE_VIEW_MODE_EDITOR,
}); });
} }
...@@ -134,8 +138,7 @@ export default { ...@@ -134,8 +138,7 @@ export default {
}, },
currentActivityView() { currentActivityView() {
if (this.currentActivityView !== leftSidebarViews.edit.name) { if (this.currentActivityView !== leftSidebarViews.edit.name) {
this.setFileViewMode({ this.updateEditor({
file: this.file,
viewMode: FILE_VIEW_MODE_EDITOR, viewMode: FILE_VIEW_MODE_EDITOR,
}); });
} }
...@@ -195,13 +198,11 @@ export default { ...@@ -195,13 +198,11 @@ export default {
'getFileData', 'getFileData',
'getRawFileData', 'getRawFileData',
'changeFileContent', 'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileViewMode',
'removePendingTab', 'removePendingTab',
'triggerFilesChange', 'triggerFilesChange',
'addTempImage', 'addTempImage',
]), ]),
...mapActions('editor', ['updateFileEditor']),
initEditor() { initEditor() {
if (this.shouldHideEditor && (this.file.content || this.file.raw)) { if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
return; return;
...@@ -284,19 +285,19 @@ export default { ...@@ -284,19 +285,19 @@ export default {
// Handle Cursor Position // Handle Cursor Position
this.editor.onPositionChange((instance, e) => { this.editor.onPositionChange((instance, e) => {
this.setEditorPosition({ this.updateEditor({
editorRow: e.position.lineNumber, editorRow: e.position.lineNumber,
editorColumn: e.position.column, editorColumn: e.position.column,
}); });
}); });
this.editor.setPosition({ this.editor.setPosition({
lineNumber: this.file.editorRow, lineNumber: this.fileEditor.editorRow,
column: this.file.editorColumn, column: this.fileEditor.editorColumn,
}); });
// Handle File Language // Handle File Language
this.setFileLanguage({ this.updateEditor({
fileLanguage: this.model.language, fileLanguage: this.model.language,
}); });
...@@ -354,6 +355,16 @@ export default { ...@@ -354,6 +355,16 @@ export default {
const schema = this.getJsonSchemaForPath(this.file.path); const schema = this.getJsonSchemaForPath(this.file.path);
registerSchema(schema); registerSchema(schema);
}, },
updateEditor(data) {
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
// when disposing. We want to ignore these by only capturing editor changes that happen to the currently active
// file.
if (!this.file.active) {
return;
}
this.updateFileEditor({ path: this.file.path, data });
},
}, },
viewerTypes, viewerTypes,
FILE_VIEW_MODE_EDITOR, FILE_VIEW_MODE_EDITOR,
...@@ -369,7 +380,7 @@ export default { ...@@ -369,7 +380,7 @@ export default {
<a <a
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_EDITOR })" @click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
...@@ -378,7 +389,7 @@ export default { ...@@ -378,7 +389,7 @@ export default {
<a <a
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_PREVIEW })" @click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
>{{ previewMode.previewTitle }}</a >{{ previewMode.previewTitle }}</a
> >
</li> </li>
......
...@@ -5,7 +5,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; ...@@ -5,7 +5,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash'; import { deprecatedCreateFlash as flash } from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { decorateFiles } from '../lib/files'; import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants'; import { stageKeys, commitActionTypes } from '../constants';
import service from '../services'; import service from '../services';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
...@@ -242,7 +242,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, ...@@ -242,7 +242,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
} }
} }
dispatch('triggerFilesChange'); dispatch('triggerFilesChange', { type: commitActionTypes.move, path, newPath });
}; };
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
......
...@@ -164,26 +164,6 @@ export const changeFileContent = ({ commit, state, getters }, { path, content }) ...@@ -164,26 +164,6 @@ export const changeFileContent = ({ commit, state, getters }, { path, content })
} }
}; };
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
if (getters.activeFile) {
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
}
};
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
editorRow,
editorColumn,
});
}
};
export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
export const restoreOriginalFile = ({ dispatch, state, commit }, path) => { export const restoreOriginalFile = ({ dispatch, state, commit }, path) => {
const file = state.entries[path]; const file = state.entries[path];
const isDestructiveDiscard = file.tempFile || file.prevPath; const isDestructiveDiscard = file.tempFile || file.prevPath;
...@@ -289,7 +269,7 @@ export const removePendingTab = ({ commit }, file) => { ...@@ -289,7 +269,7 @@ export const removePendingTab = ({ commit }, file) => {
eventHub.$emit(`editor.update.model.dispose.${file.key}`); eventHub.$emit(`editor.update.model.dispose.${file.key}`);
}; };
export const triggerFilesChange = () => { export const triggerFilesChange = (ctx, payload = {}) => {
// Used in EE for file mirroring // Used in EE for file mirroring
eventHub.$emit('ide.files.change'); eventHub.$emit('ide.files.change', payload);
}; };
...@@ -12,6 +12,8 @@ import fileTemplates from './modules/file_templates'; ...@@ -12,6 +12,8 @@ import fileTemplates from './modules/file_templates';
import paneModule from './modules/pane'; import paneModule from './modules/pane';
import clientsideModule from './modules/clientside'; import clientsideModule from './modules/clientside';
import routerModule from './modules/router'; import routerModule from './modules/router';
import editorModule from './modules/editor';
import { setupFileEditorsSync } from './modules/editor/setup';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -29,7 +31,14 @@ export const createStoreOptions = () => ({ ...@@ -29,7 +31,14 @@ export const createStoreOptions = () => ({
rightPane: paneModule(), rightPane: paneModule(),
clientside: clientsideModule(), clientside: clientsideModule(),
router: routerModule, router: routerModule,
editor: editorModule,
}, },
}); });
export const createStore = () => new Vuex.Store(createStoreOptions()); export const createStore = () => {
const store = new Vuex.Store(createStoreOptions());
setupFileEditorsSync(store);
return store;
};
import * as types from './mutation_types';
/**
* Action to update the current file editor info at the given `path` with the given `data`
*
* @param {} vuex
* @param {{ path: String, data: any }} payload
*/
export const updateFileEditor = ({ commit }, payload) => {
commit(types.UPDATE_FILE_EDITOR, payload);
};
export const removeFileEditor = ({ commit }, path) => {
commit(types.REMOVE_FILE_EDITOR, path);
};
export const renameFileEditor = ({ commit }, payload) => {
commit(types.RENAME_FILE_EDITOR, payload);
};
import { getFileEditorOrDefault } from './utils';
export const activeFileEditor = (state, getters, rootState, rootGetters) => {
const { activeFile } = rootGetters;
if (!activeFile) {
return null;
}
const { path } = rootGetters.activeFile;
return getFileEditorOrDefault(state.fileEditors, path);
};
import * as actions from './actions';
import * as getters from './getters';
import state from './state';
import mutations from './mutations';
export default {
namespaced: true,
actions,
state,
mutations,
getters,
};
export const UPDATE_FILE_EDITOR = 'UPDATE_FILE_EDITOR';
export const REMOVE_FILE_EDITOR = 'REMOVE_FILE_EDITOR';
export const RENAME_FILE_EDITOR = 'RENAME_FILE_EDITOR';
import Vue from 'vue';
import * as types from './mutation_types';
import { getFileEditorOrDefault } from './utils';
export default {
[types.UPDATE_FILE_EDITOR](state, { path, data }) {
const editor = getFileEditorOrDefault(state.fileEditors, path);
Vue.set(state.fileEditors, path, Object.assign(editor, data));
},
[types.REMOVE_FILE_EDITOR](state, path) {
Vue.delete(state.fileEditors, path);
},
[types.RENAME_FILE_EDITOR](state, { path, newPath }) {
const existing = state.fileEditors[path];
// Gracefully do nothing if fileEditor isn't found.
if (!existing) {
return;
}
Vue.delete(state.fileEditors, path);
Vue.set(state.fileEditors, newPath, existing);
},
};
import eventHub from '~/ide/eventhub';
import { commitActionTypes } from '~/ide/constants';
const removeUnusedFileEditors = store => {
Object.keys(store.state.editor.fileEditors)
.filter(path => !store.state.entries[path])
.forEach(path => store.dispatch('editor/removeFileEditor', path));
};
export const setupFileEditorsSync = store => {
eventHub.$on('ide.files.change', ({ type, ...payload } = {}) => {
if (type === commitActionTypes.move) {
store.dispatch('editor/renameFileEditor', payload);
} else {
// The files have changed, but the specific change is not known.
removeUnusedFileEditors(store);
}
});
};
export default () => ({
// Object which represents a dictionary of filePath to editor specific properties, including:
// - fileLanguage
// - editorRow
// - editorCol
// - viewMode
fileEditors: {},
});
import { FILE_VIEW_MODE_EDITOR } from '../../../constants';
export const createDefaultFileEditor = () => ({
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
viewMode: FILE_VIEW_MODE_EDITOR,
});
export const getFileEditorOrDefault = (fileEditors, path) =>
fileEditors[path] || createDefaultFileEditor();
...@@ -36,9 +36,6 @@ export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; ...@@ -36,9 +36,6 @@ export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA'; export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED'; export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
......
...@@ -95,17 +95,6 @@ export default { ...@@ -95,17 +95,6 @@ export default {
changed, changed,
}); });
}, },
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(state.entries[file.path], {
fileLanguage,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(state.entries[file.path], {
editorRow,
editorColumn,
});
},
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) { [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
let diffMode = diffModes.replaced; let diffMode = diffModes.replaced;
if (mrChange.new_file) { if (mrChange.new_file) {
...@@ -122,11 +111,6 @@ export default { ...@@ -122,11 +111,6 @@ export default {
}, },
}); });
}, },
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
Object.assign(state.entries[file.path], {
viewMode,
});
},
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path]; const entry = state.entries[path];
......
import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants'; import { commitActionTypes } from '../constants';
import { import {
relativePathToAbsolute, relativePathToAbsolute,
isAbsolute, isAbsolute,
...@@ -25,10 +25,6 @@ export const dataStructure = () => ({ ...@@ -25,10 +25,6 @@ export const dataStructure = () => ({
rawPath: '', rawPath: '',
raw: '', raw: '',
content: '', content: '',
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
viewMode: FILE_VIEW_MODE_EDITOR,
size: 0, size: 0,
parentPath: null, parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
......
...@@ -46,6 +46,20 @@ RSpec.describe 'IDE user sees editor info', :js do ...@@ -46,6 +46,20 @@ RSpec.describe 'IDE user sees editor info', :js do
end end
end end
it 'persists position after rename' do
ide_open_file('README.md')
ide_set_editor_position(4, 10)
ide_open_file('files/js/application.js')
ide_rename_file('README.md', 'READING_RAINBOW.md')
ide_open_file('READING_RAINBOW.md')
within find('.ide-status-bar') do
expect(page).to have_content('4:10')
end
end
it 'persists position' do it 'persists position' do
ide_open_file('README.md') ide_open_file('README.md')
ide_set_editor_position(4, 10) ide_set_editor_position(4, 10)
......
...@@ -6,17 +6,21 @@ import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync ...@@ -6,17 +6,21 @@ import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync
const TEST_FILE = { const TEST_FILE = {
name: 'lorem.md', name: 'lorem.md',
editorRow: 3,
editorColumn: 23,
fileLanguage: 'markdown',
content: 'abc\nndef', content: 'abc\nndef',
permalink: '/lorem.md', permalink: '/lorem.md',
}; };
const TEST_FILE_EDITOR = {
fileLanguage: 'markdown',
editorRow: 3,
editorColumn: 23,
};
const TEST_EDITOR_POSITION = `${TEST_FILE_EDITOR.editorRow}:${TEST_FILE_EDITOR.editorColumn}`;
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('ide/components/ide_status_list', () => { describe('ide/components/ide_status_list', () => {
let activeFileEditor;
let activeFile; let activeFile;
let store; let store;
let wrapper; let wrapper;
...@@ -27,6 +31,14 @@ describe('ide/components/ide_status_list', () => { ...@@ -27,6 +31,14 @@ describe('ide/components/ide_status_list', () => {
getters: { getters: {
activeFile: () => activeFile, activeFile: () => activeFile,
}, },
modules: {
editor: {
namespaced: true,
getters: {
activeFileEditor: () => activeFileEditor,
},
},
},
}); });
wrapper = shallowMount(IdeStatusList, { wrapper = shallowMount(IdeStatusList, {
...@@ -38,6 +50,7 @@ describe('ide/components/ide_status_list', () => { ...@@ -38,6 +50,7 @@ describe('ide/components/ide_status_list', () => {
beforeEach(() => { beforeEach(() => {
activeFile = TEST_FILE; activeFile = TEST_FILE;
activeFileEditor = TEST_FILE_EDITOR;
}); });
afterEach(() => { afterEach(() => {
...@@ -47,8 +60,6 @@ describe('ide/components/ide_status_list', () => { ...@@ -47,8 +60,6 @@ describe('ide/components/ide_status_list', () => {
wrapper = null; wrapper = null;
}); });
const getEditorPosition = file => `${file.editorRow}:${file.editorColumn}`;
describe('with regular file', () => { describe('with regular file', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
...@@ -65,11 +76,11 @@ describe('ide/components/ide_status_list', () => { ...@@ -65,11 +76,11 @@ describe('ide/components/ide_status_list', () => {
}); });
it('shows file editor position', () => { it('shows file editor position', () => {
expect(wrapper.text()).toContain(getEditorPosition(TEST_FILE)); expect(wrapper.text()).toContain(TEST_EDITOR_POSITION);
}); });
it('shows file language', () => { it('shows file language', () => {
expect(wrapper.text()).toContain(TEST_FILE.fileLanguage); expect(wrapper.text()).toContain(TEST_FILE_EDITOR.fileLanguage);
}); });
}); });
...@@ -81,7 +92,7 @@ describe('ide/components/ide_status_list', () => { ...@@ -81,7 +92,7 @@ describe('ide/components/ide_status_list', () => {
}); });
it('does not show file editor position', () => { it('does not show file editor position', () => {
expect(wrapper.text()).not.toContain(getEditorPosition(TEST_FILE)); expect(wrapper.text()).not.toContain(TEST_EDITOR_POSITION);
}); });
}); });
......
...@@ -55,7 +55,6 @@ describe('RepoEditor', () => { ...@@ -55,7 +55,6 @@ describe('RepoEditor', () => {
beforeEach(() => { beforeEach(() => {
const f = { const f = {
...file('file.txt'), ...file('file.txt'),
viewMode: FILE_VIEW_MODE_EDITOR,
content: 'hello world', content: 'hello world',
}; };
...@@ -92,6 +91,8 @@ describe('RepoEditor', () => { ...@@ -92,6 +91,8 @@ describe('RepoEditor', () => {
}); });
const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder'); const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder');
const changeViewMode = viewMode =>
store.dispatch('editor/updateFileEditor', { path: vm.file.path, data: { viewMode } });
describe('default', () => { describe('default', () => {
beforeEach(() => { beforeEach(() => {
...@@ -409,7 +410,7 @@ describe('RepoEditor', () => { ...@@ -409,7 +410,7 @@ describe('RepoEditor', () => {
describe('when files view mode is preview', () => { describe('when files view mode is preview', () => {
beforeEach(done => { beforeEach(done => {
jest.spyOn(vm.editor, 'updateDimensions').mockImplementation(); jest.spyOn(vm.editor, 'updateDimensions').mockImplementation();
vm.file.viewMode = FILE_VIEW_MODE_PREVIEW; changeViewMode(FILE_VIEW_MODE_PREVIEW);
vm.file.name = 'myfile.md'; vm.file.name = 'myfile.md';
vm.file.content = 'hello world'; vm.file.content = 'hello world';
...@@ -423,7 +424,7 @@ describe('RepoEditor', () => { ...@@ -423,7 +424,7 @@ describe('RepoEditor', () => {
describe('when file view mode changes to editor', () => { describe('when file view mode changes to editor', () => {
it('should update dimensions', () => { it('should update dimensions', () => {
vm.file.viewMode = FILE_VIEW_MODE_EDITOR; changeViewMode(FILE_VIEW_MODE_EDITOR);
return vm.$nextTick().then(() => { return vm.$nextTick().then(() => {
expect(vm.editor.updateDimensions).toHaveBeenCalled(); expect(vm.editor.updateDimensions).toHaveBeenCalled();
......
import * as pathUtils from 'path'; import * as pathUtils from 'path';
import { decorateData } from '~/ide/stores/utils'; import { decorateData } from '~/ide/stores/utils';
import { commitActionTypes } from '~/ide/constants';
export const file = (name = 'name', id = name, type = '', parent = null) => export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({ decorateData({
...@@ -28,3 +29,17 @@ export const createEntriesFromPaths = paths => ...@@ -28,3 +29,17 @@ export const createEntriesFromPaths = paths =>
...entries, ...entries,
}; };
}, {}); }, {});
export const createTriggerChangeAction = payload => ({
type: 'triggerFilesChange',
...(payload ? { payload } : {}),
});
export const createTriggerRenamePayload = (path, newPath) => ({
type: commitActionTypes.move,
path,
newPath,
});
export const createTriggerRenameAction = (path, newPath) =>
createTriggerChangeAction(createTriggerRenamePayload(path, newPath));
...@@ -7,7 +7,7 @@ import * as types from '~/ide/stores/mutation_types'; ...@@ -7,7 +7,7 @@ import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services'; import service from '~/ide/services';
import { createRouter } from '~/ide/ide_router'; import { createRouter } from '~/ide/ide_router';
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
import { file } from '../../helpers'; import { file, createTriggerRenameAction } from '../../helpers';
const ORIGINAL_CONTENT = 'original content'; const ORIGINAL_CONTENT = 'original content';
const RELATIVE_URL_ROOT = '/gitlab'; const RELATIVE_URL_ROOT = '/gitlab';
...@@ -785,13 +785,19 @@ describe('IDE store file actions', () => { ...@@ -785,13 +785,19 @@ describe('IDE store file actions', () => {
}); });
describe('triggerFilesChange', () => { describe('triggerFilesChange', () => {
const { payload: renamePayload } = createTriggerRenameAction('test', '123');
beforeEach(() => { beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
}); });
it('emits event that files have changed', () => { it.each`
return store.dispatch('triggerFilesChange').then(() => { args | payload
expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change'); ${[]} | ${{}}
${[renamePayload]} | ${renamePayload}
`('emits event that files have changed (args=$args)', ({ args, payload }) => {
return store.dispatch('triggerFilesChange', ...args).then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change', payload);
}); });
}); });
}); });
......
...@@ -19,7 +19,7 @@ import { ...@@ -19,7 +19,7 @@ import {
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
import { file } from '../helpers'; import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
...@@ -522,7 +522,7 @@ describe('Multi-file store actions', () => { ...@@ -522,7 +522,7 @@ describe('Multi-file store actions', () => {
'path', 'path',
store.state, store.state,
[{ type: types.DELETE_ENTRY, payload: 'path' }], [{ type: types.DELETE_ENTRY, payload: 'path' }],
[{ type: 'stageChange', payload: 'path' }, { type: 'triggerFilesChange' }], [{ type: 'stageChange', payload: 'path' }, createTriggerChangeAction()],
done, done,
); );
}); });
...@@ -551,7 +551,7 @@ describe('Multi-file store actions', () => { ...@@ -551,7 +551,7 @@ describe('Multi-file store actions', () => {
[{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }], [{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }],
[ [
{ type: 'stageChange', payload: 'testFolder/entry-to-delete' }, { type: 'stageChange', payload: 'testFolder/entry-to-delete' },
{ type: 'triggerFilesChange' }, createTriggerChangeAction(),
], ],
done, done,
); );
...@@ -614,7 +614,7 @@ describe('Multi-file store actions', () => { ...@@ -614,7 +614,7 @@ describe('Multi-file store actions', () => {
testEntry.path, testEntry.path,
store.state, store.state,
[{ type: types.DELETE_ENTRY, payload: testEntry.path }], [{ type: types.DELETE_ENTRY, payload: testEntry.path }],
[{ type: 'stageChange', payload: testEntry.path }, { type: 'triggerFilesChange' }], [{ type: 'stageChange', payload: testEntry.path }, createTriggerChangeAction()],
done, done,
); );
}); });
...@@ -754,7 +754,7 @@ describe('Multi-file store actions', () => { ...@@ -754,7 +754,7 @@ describe('Multi-file store actions', () => {
payload: origEntry, payload: origEntry,
}, },
], ],
[{ type: 'triggerFilesChange' }], [createTriggerRenameAction('renamed', 'orig')],
done, done,
); );
}); });
...@@ -767,7 +767,7 @@ describe('Multi-file store actions', () => { ...@@ -767,7 +767,7 @@ describe('Multi-file store actions', () => {
{ path: 'orig', name: 'renamed' }, { path: 'orig', name: 'renamed' },
store.state, store.state,
[expect.objectContaining({ type: types.RENAME_ENTRY })], [expect.objectContaining({ type: types.RENAME_ENTRY })],
[{ type: 'triggerFilesChange' }], [createTriggerRenameAction('orig', 'renamed')],
done, done,
); );
}); });
......
import testAction from 'helpers/vuex_action_helper';
import * as types from '~/ide/stores/modules/editor/mutation_types';
import * as actions from '~/ide/stores/modules/editor/actions';
import { createTriggerRenamePayload } from '../../../helpers';
describe('~/ide/stores/modules/editor/actions', () => {
describe('updateFileEditor', () => {
it('commits with payload', () => {
const payload = {};
testAction(actions.updateFileEditor, payload, {}, [
{ type: types.UPDATE_FILE_EDITOR, payload },
]);
});
});
describe('removeFileEditor', () => {
it('commits with payload', () => {
const payload = 'path/to/file.txt';
testAction(actions.removeFileEditor, payload, {}, [
{ type: types.REMOVE_FILE_EDITOR, payload },
]);
});
});
describe('renameFileEditor', () => {
it('commits with payload', () => {
const payload = createTriggerRenamePayload('test', 'test123');
testAction(actions.renameFileEditor, payload, {}, [
{ type: types.RENAME_FILE_EDITOR, payload },
]);
});
});
});
import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import * as getters from '~/ide/stores/modules/editor/getters';
const TEST_PATH = 'test/path.md';
const TEST_FILE_EDITOR = {
...createDefaultFileEditor(),
editorRow: 7,
editorColumn: 8,
fileLanguage: 'markdown',
};
describe('~/ide/stores/modules/editor/getters', () => {
describe('activeFileEditor', () => {
it.each`
activeFile | fileEditors | expected
${null} | ${{}} | ${null}
${{}} | ${{}} | ${createDefaultFileEditor()}
${{ path: TEST_PATH }} | ${{}} | ${createDefaultFileEditor()}
${{ path: TEST_PATH }} | ${{ bogus: createDefaultFileEditor(), [TEST_PATH]: TEST_FILE_EDITOR }} | ${TEST_FILE_EDITOR}
`(
'with activeFile=$activeFile and fileEditors=$fileEditors',
({ activeFile, fileEditors, expected }) => {
const rootGetters = { activeFile };
const state = { fileEditors };
const result = getters.activeFileEditor(state, {}, {}, rootGetters);
expect(result).toEqual(expected);
},
);
});
});
import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import * as types from '~/ide/stores/modules/editor/mutation_types';
import mutations from '~/ide/stores/modules/editor/mutations';
import { createTriggerRenamePayload } from '../../../helpers';
const TEST_PATH = 'test/path.md';
describe('~/ide/stores/modules/editor/mutations', () => {
describe(types.UPDATE_FILE_EDITOR, () => {
it('with path that does not exist, should initialize with default values', () => {
const state = { fileEditors: {} };
const data = { fileLanguage: 'markdown' };
mutations[types.UPDATE_FILE_EDITOR](state, { path: TEST_PATH, data });
expect(state.fileEditors).toEqual({
[TEST_PATH]: {
...createDefaultFileEditor(),
...data,
},
});
});
it('with existing path, should overwrite values', () => {
const state = {
fileEditors: {
foo: {},
[TEST_PATH]: { ...createDefaultFileEditor(), editorRow: 7, editorColumn: 7 },
},
};
mutations[types.UPDATE_FILE_EDITOR](state, {
path: TEST_PATH,
data: { fileLanguage: 'markdown' },
});
expect(state).toEqual({
fileEditors: {
foo: {},
[TEST_PATH]: {
...createDefaultFileEditor(),
editorRow: 7,
editorColumn: 7,
fileLanguage: 'markdown',
},
},
});
});
});
describe(types.REMOVE_FILE_EDITOR, () => {
it.each`
fileEditors | path | expected
${{}} | ${'does/not/exist.txt'} | ${{}}
${{ foo: {}, [TEST_PATH]: {} }} | ${TEST_PATH} | ${{ foo: {} }}
`('removes file $path', ({ fileEditors, path, expected }) => {
const state = { fileEditors };
mutations[types.REMOVE_FILE_EDITOR](state, path);
expect(state).toEqual({ fileEditors: expected });
});
});
describe(types.RENAME_FILE_EDITOR, () => {
it.each`
fileEditors | payload | expected
${{ foo: {} }} | ${createTriggerRenamePayload('does/not/exist', 'abc')} | ${{ foo: {} }}
${{ foo: { a: 1 }, bar: {} }} | ${createTriggerRenamePayload('foo', 'abc/def')} | ${{ 'abc/def': { a: 1 }, bar: {} }}
`('renames fileEditor at $payload', ({ fileEditors, payload, expected }) => {
const state = { fileEditors };
mutations[types.RENAME_FILE_EDITOR](state, payload);
expect(state).toEqual({ fileEditors: expected });
});
});
});
import Vuex from 'vuex';
import eventHub from '~/ide/eventhub';
import { createStoreOptions } from '~/ide/stores';
import { setupFileEditorsSync } from '~/ide/stores/modules/editor/setup';
import { createTriggerRenamePayload } from '../../../helpers';
describe('~/ide/stores/modules/editor/setup', () => {
let store;
beforeEach(() => {
store = new Vuex.Store(createStoreOptions());
store.state.entries = {
foo: {},
bar: {},
};
store.state.editor.fileEditors = {
foo: {},
bizz: {},
};
setupFileEditorsSync(store);
});
it('when files change is emitted, removes unused fileEditors', () => {
eventHub.$emit('ide.files.change');
expect(store.state.entries).toEqual({
foo: {},
bar: {},
});
expect(store.state.editor.fileEditors).toEqual({
foo: {},
});
});
it('when files rename is emitted, renames fileEditor', () => {
eventHub.$emit('ide.files.change', createTriggerRenamePayload('foo', 'foo_new'));
expect(store.state.editor.fileEditors).toEqual({
foo_new: {},
bizz: {},
});
});
});
import mutations from '~/ide/stores/mutations/file'; import mutations from '~/ide/stores/mutations/file';
import { createStore } from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants';
import { file } from '../../helpers'; import { file } from '../../helpers';
describe('IDE store file mutations', () => { describe('IDE store file mutations', () => {
...@@ -532,17 +531,6 @@ describe('IDE store file mutations', () => { ...@@ -532,17 +531,6 @@ describe('IDE store file mutations', () => {
}); });
}); });
describe('SET_FILE_VIEWMODE', () => {
it('updates file view mode', () => {
mutations.SET_FILE_VIEWMODE(localState, {
file: localFile,
viewMode: FILE_VIEW_MODE_PREVIEW,
});
expect(localFile.viewMode).toBe(FILE_VIEW_MODE_PREVIEW);
});
});
describe('ADD_PENDING_TAB', () => { describe('ADD_PENDING_TAB', () => {
beforeEach(() => { beforeEach(() => {
const f = { ...file('openFile'), path: 'openFile', active: true, opened: true }; const f = { ...file('openFile'), path: 'openFile', active: true, opened: true };
......
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