Commit 174c2f80 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-code-cleanup

parents 2a7fedaf 97c7c8ec
...@@ -73,6 +73,7 @@ export default class MergeRequestTabs { ...@@ -73,6 +73,7 @@ export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix'); const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab'); const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('peek');
const paddingTop = 16; const paddingTop = 16;
this.diffsLoaded = false; this.diffsLoaded = false;
...@@ -86,6 +87,10 @@ export default class MergeRequestTabs { ...@@ -86,6 +87,10 @@ export default class MergeRequestTabs {
this.showTab = this.showTab.bind(this); this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
if (mergeRequestTabs) { if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight; this.stickyTop += mergeRequestTabs.offsetHeight;
} }
......
...@@ -132,13 +132,35 @@ ...@@ -132,13 +132,35 @@
.multi-file-tabs { .multi-file-tabs {
display: flex; display: flex;
overflow-x: auto;
background-color: $white-normal; background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark; box-shadow: inset 0 -1px $white-dark;
> li { > ul {
display: flex;
overflow-x: auto;
}
li {
position: relative; position: relative;
} }
.dropdown {
display: flex;
margin-left: auto;
margin-bottom: 1px;
padding: 0 $grid-size;
border-left: 1px solid $white-dark;
background-color: $white-light;
&.shadow {
box-shadow: 0 0 10px $dropdown-shadow-color;
}
.btn {
margin-top: auto;
margin-bottom: auto;
}
}
} }
.multi-file-tab { .multi-file-tab {
...@@ -207,6 +229,70 @@ ...@@ -207,6 +229,70 @@
.vertical-center { .vertical-center {
min-height: auto; min-height: auto;
} }
.monaco-editor .lines-content .cigr {
display: none;
}
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
}
.diagonal-fill {
display: none !important;
}
.diffOverview {
background-color: $white-light;
border-left: 1px solid $white-dark;
cursor: ns-resize;
}
.diffViewport {
display: none;
}
.char-insert {
background-color: $line-added-dark;
}
.char-delete {
background-color: $line-removed-dark;
}
.line-numbers {
color: $black-transparent;
}
.view-overlays {
.line-insert {
background-color: $line-added;
}
.line-delete {
background-color: $line-removed;
}
}
.margin {
background-color: $gray-light;
border-right: 1px solid $white-normal;
.line-insert {
border-right: 1px solid $line-added-dark;
}
.line-delete {
border-right: 1px solid $line-removed-dark;
}
}
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: .4;
}
}
} }
.multi-file-editor-holder { .multi-file-editor-holder {
......
...@@ -127,7 +127,11 @@ keys must be manually replicated to the secondary node. ...@@ -127,7 +127,11 @@ keys must be manually replicated to the secondary node.
1. Restart sshd: 1. Restart sshd:
```bash ```bash
service ssh restart # Debian or Ubuntu installations
sudo service ssh reload
# CentOS installations
sudo service sshd reload
``` ```
### Step 3. Add the secondary GitLab node ### Step 3. Add the secondary GitLab node
......
...@@ -16,18 +16,26 @@ codequality: ...@@ -16,18 +16,26 @@ codequality:
- docker:dind - docker:dind
script: script:
- docker pull codeclimate/codeclimate - docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true - docker run
--env SOURCE_CODE="$PWD" \
--volume "$PWD":/code \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
``` ```
This will create a `codequality` job in your CI pipeline and will allow you to The above example will create a `codequality` job in your CI/CD pipeline which
download and analyze the report artifact in JSON format. will scan your source code for code quality issues. The report will be saved
as an artifact that you can later download and analyze.
For [GitLab Starter][ee] users, this information can be automatically TIP: **Tip:**
extracted and shown right in the merge request widget. [Learn more on code quality Starting with [GitLab Starter][ee] 9.3, this information will
diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md). be automatically extracted and shown right in the merge request widget. To do
so, the CI/CD job must be named `codequality` and the artifact path must be
`codeclimate.json`.
[Learn more on code quality diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md).
[cli]: https://github.com/codeclimate/codeclimate [cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor [dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
......
This diff is collapsed.
This diff is collapsed.
...@@ -24,8 +24,11 @@ ...@@ -24,8 +24,11 @@
methods: { methods: {
...mapActions([ ...mapActions([
'discardFileChanges', 'discardFileChanges',
'updateViewer',
]), ]),
openFileInEditor(file) { openFileInEditor(file) {
this.updateViewer('diff');
router.push(`/project${file.url}`); router.push(`/project${file.url}`);
}, },
}, },
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
:class="{
'is-active': viewer === 'editor',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
:class="{
'is-active': viewer === 'diff',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the last commit') }}
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
...@@ -31,17 +31,12 @@ ...@@ -31,17 +31,12 @@
}, },
}, },
computed: { computed: {
...mapState([ ...mapState(['changedFiles', 'openFiles', 'viewer']),
'changedFiles', ...mapGetters(['activeFile', 'hasChanges']),
'openFiles',
]),
...mapGetters([
'activeFile',
]),
}, },
mounted() { mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?'; const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => { window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined; if (!this.changedFiles.length) return undefined;
Object.assign(e, { Object.assign(e, {
...@@ -66,6 +61,8 @@ ...@@ -66,6 +61,8 @@
> >
<repo-tabs <repo-tabs
:files="openFiles" :files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
/> />
<repo-editor <repo-editor
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
......
...@@ -16,6 +16,8 @@ export default { ...@@ -16,6 +16,8 @@ export default {
...mapState([ ...mapState([
'leftPanelCollapsed', 'leftPanelCollapsed',
'rightPanelCollapsed', 'rightPanelCollapsed',
'viewer',
'delayViewerUpdated',
]), ]),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw; return this.file && this.file.binary && !this.file.raw;
...@@ -33,6 +35,9 @@ export default { ...@@ -33,6 +35,9 @@ export default {
rightPanelCollapsed() { rightPanelCollapsed() {
this.editor.updateDimensions(); this.editor.updateDimensions();
}, },
viewer() {
this.createEditorInstance();
},
}, },
beforeDestroy() { beforeDestroy() {
this.editor.dispose(); this.editor.dispose();
...@@ -55,6 +60,8 @@ export default { ...@@ -55,6 +60,8 @@ export default {
'setFileLanguage', 'setFileLanguage',
'setEditorPosition', 'setEditorPosition',
'setFileEOL', 'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -63,16 +70,34 @@ export default { ...@@ -63,16 +70,34 @@ export default {
this.getRawFileData(this.file) this.getRawFileData(this.file)
.then(() => { .then(() => {
this.editor.createInstance(this.$refs.editor); const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
}) })
.then(() => this.setupEditor())
.catch((err) => { .catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err; throw err;
}); });
}, },
createEditorInstance() {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
}
this.setupEditor();
});
},
setupEditor() { setupEditor() {
if (!this.file) return; if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.file); this.model = this.editor.createModel(this.file);
......
...@@ -52,15 +52,23 @@ ...@@ -52,15 +52,23 @@
} }
}, },
methods: { methods: {
...mapActions([ ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
'toggleTreeOpen',
]),
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) { if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
const delayPromise = this.file.changed
? 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 RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default { export default {
components: { components: {
RepoTab, RepoTab,
EditorMode,
}, },
props: { props: {
files: { files: {
type: Array, type: Array,
required: true, required: true,
}, },
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
},
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
}, },
}; };
</script> </script>
<template> <template>
<div class="multi-file-tabs">
<ul <ul
class="multi-file-tabs list-unstyled append-bottom-0" class="list-unstyled append-bottom-0"
ref="tabsScroller"
> >
<repo-tab <repo-tab
v-for="file in files" v-for="tab in files"
:key="file.key" :key="tab.key"
:tab="file" :tab="tab"
/> />
</ul> </ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
@click="updateViewer"
/>
</div>
</template> </template>
...@@ -26,6 +26,9 @@ export default class Model { ...@@ -26,6 +26,9 @@ export default class Model {
this.events = new Map(); this.events = new Map();
this.updateContent = this.updateContent.bind(this); 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.content.${this.file.path}`, this.updateContent); eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
...@@ -75,6 +78,7 @@ export default class Model { ...@@ -75,6 +78,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.content.${this.file.path}`, this.updateContent); eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
} }
import eventHub from 'ee/ide/eventhub';
import Disposable from './disposable'; import Disposable from './disposable';
import Model from './model'; import Model from './model';
...@@ -25,9 +26,17 @@ export default class ModelManager { ...@@ -25,9 +26,17 @@ export default class ModelManager {
this.models.set(model.path, model); this.models.set(model.path, model);
this.disposable.add(model); this.disposable.add(model);
eventHub.$on(`editor.update.model.dispose.${file.path}`, this.removeCachedModel.bind(this, file));
return model; return model;
} }
removeCachedModel(file) {
this.models.delete(file.path);
eventHub.$off(`editor.update.model.dispose.${file.path}`, this.removeCachedModel);
}
dispose() { dispose() {
// dispose of all the models // dispose of all the models
this.disposable.dispose(); this.disposable.dispose();
......
...@@ -27,6 +27,8 @@ export default class DecorationsController { ...@@ -27,6 +27,8 @@ export default class DecorationsController {
} }
decorate(model) { decorate(model) {
if (!this.editor.instance) return;
const decorations = this.getAllDecorationsForModel(model); const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || []; const oldDecorations = this.editorDecorations.get(model.url) || [];
......
...@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller'; ...@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller'; import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable'; import Disposable from './common/disposable';
import ModelManager from './common/model_manager'; import ModelManager from './common/model_manager';
import editorOptions from './editor_options'; import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import gitlabTheme from 'ee/ide/lib/themes/gl_theme'; // eslint-disable-line import/first export const clearDomElement = el => {
if (!el || !el.firstChild) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
export default class Editor { export default class Editor {
static create(monaco) { static create(monaco) {
...@@ -34,19 +41,31 @@ export default class Editor { ...@@ -34,19 +41,31 @@ export default class Editor {
createInstance(domElement) { createInstance(domElement) {
if (!this.instance) { if (!this.instance) {
clearDomElement(domElement);
this.disposable.add( this.disposable.add(
this.instance = this.monaco.editor.create(domElement, { (this.instance = this.monaco.editor.create(domElement, {
model: null, ...defaultEditorOptions,
readOnly: false, })),
contextmenu: true, (this.dirtyDiffController = new DirtyDiffController(
scrollBeyondLastLine: false, this.modelManager,
minimap: { this.decorationsController,
enabled: false, )),
}, );
}),
this.dirtyDiffController = new DirtyDiffController( window.addEventListener('resize', this.debouncedUpdate, false);
this.modelManager, this.decorationsController, }
), }
createDiffInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
})),
); );
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
...@@ -58,25 +77,39 @@ export default class Editor { ...@@ -58,25 +77,39 @@ export default class Editor {
} }
attachModel(model) { attachModel(model) {
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
});
return;
}
this.instance.setModel(model.getModel()); this.instance.setModel(model.getModel());
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model; this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => { this.instance.updateOptions(
Object.keys(obj).forEach((key) => { editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach(key => {
Object.assign(acc, { Object.assign(acc, {
[key]: obj[key](model), [key]: obj[key](model),
}); });
}); });
return acc; return acc;
}, {})); }, {}),
);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
setupMonacoTheme() { setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab'); this.monaco.editor.setTheme('gitlab');
} }
...@@ -88,12 +121,21 @@ export default class Editor { ...@@ -88,12 +121,21 @@ export default class Editor {
} }
dispose() { dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate); window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance // catch any potential errors with disposing the error
if (this.instance) { // this is mainly for tests caused by elements not existing
try {
this.disposable.dispose();
this.instance = null;
} catch (e) {
this.instance = null; this.instance = null;
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line no-console
console.error(e);
}
} }
} }
...@@ -113,6 +155,8 @@ export default class Editor { ...@@ -113,6 +155,8 @@ export default class Editor {
} }
onPositionChange(cb) { onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add( this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)), this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
); );
......
export default [{ export const defaultEditorOptions = {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
};
export default [
{
readOnly: model => !!model.file.file_lock, readOnly: model => !!model.file.file_lock,
}]; },
];
...@@ -6,6 +6,9 @@ export default { ...@@ -6,6 +6,9 @@ export default {
rules: [], rules: [],
colors: { colors: {
'editorLineNumber.foreground': '#CCCCCC', 'editorLineNumber.foreground': '#CCCCCC',
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
}, },
}, },
}; };
...@@ -108,6 +108,14 @@ export const scrollToTab = () => { ...@@ -108,6 +108,14 @@ export const scrollToTab = () => {
}); });
}; };
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash'; import flash from '~/flash';
import eventHub from 'ee/ide/eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { import { setPageTitle } from '../utils';
setPageTitle,
} from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => { export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.indexOf(path); const indexOfClosedFile = state.openFiles.indexOf(path);
...@@ -23,6 +22,8 @@ export const closeFile = ({ commit, state, getters, dispatch }, path) => { ...@@ -23,6 +22,8 @@ export const closeFile = ({ commit, state, getters, dispatch }, path) => {
} 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}`);
}; };
export const setFileActive = ({ commit, state, getters, dispatch }, path) => { export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
...@@ -32,7 +33,10 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -32,7 +33,10 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
if (file.active) return; if (file.active) return;
if (currentActiveFile) { if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { path: currentActiveFile.path, active: false }); commit(types.SET_FILE_ACTIVE, {
path: currentActiveFile.path,
active: false,
});
} }
commit(types.SET_FILE_ACTIVE, { path, active: true }); commit(types.SET_FILE_ACTIVE, { path, active: true });
...@@ -45,15 +49,16 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -45,15 +49,16 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, file) => { export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
return service.getFileData(file.url) return service
.then((res) => { .getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle); setPageTitle(pageTitle);
return res.json(); return res.json();
}) })
.then((data) => { .then(data => {
commit(types.SET_FILE_DATA, { data, file }); commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path); commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path); dispatch('setFileActive', file.path);
...@@ -61,15 +66,33 @@ export const getFileData = ({ state, commit, dispatch }, file) => { ...@@ -61,15 +66,33 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
flash('Error loading file data. Please try again.', 'alert', document, null, false, true); flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
}); });
}; };
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file) export const getRawFileData = ({ commit, dispatch }, file) =>
.then((raw) => { service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); commit(types.SET_FILE_RAW_DATA, { file, raw });
}) })
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true)); .catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
export const changeFileContent = ({ state, commit }, { path, content }) => { export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
...@@ -96,9 +119,16 @@ export const setFileEOL = ({ getters, commit }, { eol }) => { ...@@ -96,9 +119,16 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
} }
}; };
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => { export const setEditorPosition = (
{ getters, commit },
{ editorRow, editorColumn },
) => {
if (getters.activeFile) { if (getters.activeFile) {
commit(types.SET_FILE_POSITION, { file: getters.activeFile, editorRow, editorColumn }); commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
editorRow,
editorColumn,
});
} }
}; };
...@@ -111,4 +141,6 @@ export const discardFileChanges = ({ state, commit }, path) => { ...@@ -111,4 +141,6 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) { if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path); commit(types.TOGGLE_FILE_OPEN, path);
} }
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
}; };
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state =>
state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile); export const modifiedFiles = state =>
state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state => Object.keys(state.projects).map((projectId) => { export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
const project = state.projects[projectId]; const project = state.projects[projectId];
return { return {
...project, ...project,
branches: Object.keys(project.branches).map((branchId) => { branches: Object.keys(project.branches).map(branchId => {
const branch = project.branches[branchId]; const branch = project.branches[branchId];
return { return {
...@@ -18,7 +21,10 @@ export const projectsWithTrees = state => Object.keys(state.projects).map((proje ...@@ -18,7 +21,10 @@ export const projectsWithTrees = state => Object.keys(state.projects).map((proje
}; };
}), }),
}; };
}); });
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state => export const currentIcon = state =>
(state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'); state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
...@@ -36,9 +36,8 @@ export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; ...@@ -36,9 +36,8 @@ 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';
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES'; export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
...@@ -11,7 +11,10 @@ export default { ...@@ -11,7 +11,10 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) { if (entry.path) {
Object.assign(state.entries[entry.path], { Object.assign(state.entries[entry.path], {
loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading, loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
}); });
} else { } else {
Object.assign(entry, { Object.assign(entry, {
...@@ -63,8 +66,8 @@ export default { ...@@ -63,8 +66,8 @@ export default {
[key]: entry, [key]: entry,
}); });
} else { } else {
const tree = entry.tree.filter(f => const tree = entry.tree.filter(
foundEntry.tree.find(e => e.path === f.path) === undefined, f => foundEntry.tree.find(e => e.path === f.path) === undefined,
); );
Object.assign(foundEntry, { Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree), tree: foundEntry.tree.concat(tree),
...@@ -74,14 +77,28 @@ export default { ...@@ -74,14 +77,28 @@ export default {
return acc.concat(key); return acc.concat(key);
}, []); }, []);
const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(e => e.path === data.treeList[0].path); const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
e => e.path === data.treeList[0].path,
);
if (!foundEntry) { if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], { Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList), tree: state.trees[`${projectId}/${branchId}`].tree.concat(
data.treeList,
),
}); });
} }
}, },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
});
},
[types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) {
Object.assign(state, {
delayViewerUpdated,
});
},
...projectMutations, ...projectMutations,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
......
...@@ -14,4 +14,6 @@ export default () => ({ ...@@ -14,4 +14,6 @@ export default () => ({
rightPanelCollapsed: false, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
entries: {}, entries: {},
viewer: 'editor',
delayViewerUpdated: false,
}); });
...@@ -36,6 +36,7 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -36,6 +36,7 @@ describe('Multi-file editor commit sidebar list item', () => {
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', () => {
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();
...@@ -44,6 +45,16 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -44,6 +45,16 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(router.push).toHaveBeenCalled(); expect(router.push).toHaveBeenCalled();
}); });
it('calls updateViewer with diff when clicking file', () => {
spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
expect(vm.updateViewer).toHaveBeenCalledWith('diff');
});
describe('computed', () => { describe('computed', () => {
describe('iconName', () => { describe('iconName', () => {
it('returns modified when not a tempFile', () => { it('returns modified when not a tempFile', () => {
......
...@@ -63,6 +63,34 @@ describe('RepoEditor', () => { ...@@ -63,6 +63,34 @@ describe('RepoEditor', () => {
}); });
}); });
describe('createEditorInstance', () => {
it('calls createInstance when viewer is editor', (done) => {
spyOn(vm.editor, 'createInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createInstance).toHaveBeenCalled();
done();
});
});
it('calls createDiffInstance when viewer is diff', (done) => {
vm.$store.state.viewer = 'diff';
spyOn(vm.editor, 'createDiffInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createDiffInstance).toHaveBeenCalled();
done();
});
});
});
describe('setupEditor', () => { describe('setupEditor', () => {
it('creates new model', () => { it('creates new model', () => {
spyOn(vm.editor, 'createModel').and.callThrough(); spyOn(vm.editor, 'createModel').and.callThrough();
......
...@@ -12,9 +12,11 @@ describe('RepoTabs', () => { ...@@ -12,9 +12,11 @@ describe('RepoTabs', () => {
vm.$destroy(); vm.$destroy();
}); });
it('renders a list of tabs', (done) => { it('renders a list of tabs', done => {
vm = createComponent(RepoTabs, { vm = createComponent(RepoTabs, {
files: openedFiles, files: openedFiles,
viewer: 'editor',
hasChanges: false,
}); });
openedFiles[0].active = true; openedFiles[0].active = true;
...@@ -28,4 +30,52 @@ describe('RepoTabs', () => { ...@@ -28,4 +30,52 @@ describe('RepoTabs', () => {
done(); done();
}); });
}); });
describe('updated', () => {
it('sets showShadow as true when scroll width is larger than width', done => {
const el = document.createElement('div');
el.innerHTML = '<div id="test-app"></div>';
document.body.appendChild(el);
const style = document.createElement('style');
style.innerText = `
.multi-file-tabs {
width: 100px;
}
.multi-file-tabs .list-unstyled {
display: flex;
overflow-x: auto;
}
`;
document.head.appendChild(style);
vm = createComponent(
RepoTabs,
{
files: [],
viewer: 'editor',
hasChanges: false,
},
'#test-app',
);
vm
.$nextTick()
.then(() => {
expect(vm.showShadow).toEqual(false);
vm.files = openedFiles;
})
.then(vm.$nextTick)
.then(() => {
expect(vm.showShadow).toEqual(true);
style.remove();
el.remove();
})
.then(done)
.catch(done.fail);
});
});
}); });
/* global monaco */ /* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader'; import monacoLoader from 'ee/ide/monaco_loader';
import ModelManager from 'ee/ide/lib/common/model_manager'; import ModelManager from 'ee/ide/lib/common/model_manager';
import { file } from '../../helpers'; import { file } from '../../helpers';
...@@ -47,6 +48,15 @@ describe('Multi-file editor library model manager', () => { ...@@ -47,6 +48,15 @@ describe('Multi-file editor library model manager', () => {
expect(instance.models.get).toHaveBeenCalled(); expect(instance.models.get).toHaveBeenCalled();
}); });
it('adds eventHub listener', () => {
const f = file();
spyOn(eventHub, '$on').and.callThrough();
instance.addModel(f);
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything());
});
}); });
describe('hasCachedModel', () => { describe('hasCachedModel', () => {
...@@ -69,6 +79,30 @@ describe('Multi-file editor library model manager', () => { ...@@ -69,6 +79,30 @@ describe('Multi-file editor library model manager', () => {
}); });
}); });
describe('removeCachedModel', () => {
let f;
beforeEach(() => {
f = file();
instance.addModel(f);
});
it('clears cached model', () => {
instance.removeCachedModel(f);
expect(instance.models.size).toBe(0);
});
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
instance.removeCachedModel(f);
expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything());
});
});
describe('dispose', () => { describe('dispose', () => {
it('clears cached models', () => { it('clears cached models', () => {
instance.addModel(file()); instance.addModel(file());
......
/* global monaco */ /* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader'; import monacoLoader from 'ee/ide/monaco_loader';
import Model from 'ee/ide/lib/common/model'; import Model from 'ee/ide/lib/common/model';
import { file } from '../../helpers'; import { file } from '../../helpers';
...@@ -7,6 +8,8 @@ describe('Multi-file editor library model', () => { ...@@ -7,6 +8,8 @@ describe('Multi-file editor library model', () => {
let model; let model;
beforeEach((done) => { beforeEach((done) => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path')); model = new Model(monaco, file('path'));
...@@ -23,6 +26,10 @@ describe('Multi-file editor library model', () => { ...@@ -23,6 +26,10 @@ describe('Multi-file editor library model', () => {
expect(model.model).not.toBeNull(); expect(model.model).not.toBeNull();
}); });
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
describe('path', () => { describe('path', () => {
it('returns file path', () => { it('returns file path', () => {
expect(model.path).toBe('path'); expect(model.path).toBe('path');
...@@ -88,5 +95,13 @@ describe('Multi-file editor library model', () => { ...@@ -88,5 +95,13 @@ describe('Multi-file editor library model', () => {
expect(model.events.size).toBe(0); expect(model.events.size).toBe(0);
}); });
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
model.dispose();
expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
}); });
}); });
...@@ -5,8 +5,16 @@ import { file } from '../helpers'; ...@@ -5,8 +5,16 @@ import { file } from '../helpers';
describe('Multi-file editor library', () => { describe('Multi-file editor library', () => {
let instance; let instance;
let el;
let holder;
beforeEach(done => {
el = document.createElement('div');
holder = document.createElement('div');
el.appendChild(holder);
document.body.appendChild(el);
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
instance = editor.create(monaco); instance = editor.create(monaco);
...@@ -16,6 +24,8 @@ describe('Multi-file editor library', () => { ...@@ -16,6 +24,8 @@ describe('Multi-file editor library', () => {
afterEach(() => { afterEach(() => {
instance.dispose(); instance.dispose();
el.remove();
}); });
it('creates instance of editor', () => { it('creates instance of editor', () => {
...@@ -27,33 +37,48 @@ describe('Multi-file editor library', () => { ...@@ -27,33 +37,48 @@ describe('Multi-file editor library', () => {
}); });
describe('createInstance', () => { describe('createInstance', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
it('creates editor instance', () => { it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough(); spyOn(instance.monaco.editor, 'create').and.callThrough();
instance.createInstance(el); instance.createInstance(holder);
expect(instance.monaco.editor.create).toHaveBeenCalled(); expect(instance.monaco.editor.create).toHaveBeenCalled();
}); });
it('creates dirty diff controller', () => { it('creates dirty diff controller', () => {
instance.createInstance(el); instance.createInstance(holder);
expect(instance.dirtyDiffController).not.toBeNull(); expect(instance.dirtyDiffController).not.toBeNull();
}); });
it('creates model manager', () => { it('creates model manager', () => {
instance.createInstance(el); instance.createInstance(holder);
expect(instance.modelManager).not.toBeNull(); expect(instance.modelManager).not.toBeNull();
}); });
}); });
describe('createDiffInstance', () => {
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
instance.createDiffInstance(holder);
expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(
holder,
{
model: null,
contextmenu: true,
minimap: {
enabled: false,
},
readOnly: true,
scrollBeyondLastLine: false,
},
);
});
});
describe('createModel', () => { describe('createModel', () => {
it('calls model manager addModel', () => { it('calls model manager addModel', () => {
spyOn(instance.modelManager, 'addModel'); spyOn(instance.modelManager, 'addModel');
...@@ -87,12 +112,28 @@ describe('Multi-file editor library', () => { ...@@ -87,12 +112,28 @@ describe('Multi-file editor library', () => {
expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
}); });
it('sets original & modified when diff editor', () => {
spyOn(instance.instance, 'getEditorType').and.returnValue(
'vs.editor.IDiffEditor',
);
spyOn(instance.instance, 'setModel');
instance.attachModel(model);
expect(instance.instance.setModel).toHaveBeenCalledWith({
original: model.getOriginalModel(),
modified: model.getModel(),
});
});
it('attaches the model to the dirty diff controller', () => { it('attaches the model to the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'attachModel'); spyOn(instance.dirtyDiffController, 'attachModel');
instance.attachModel(model); instance.attachModel(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(
model,
);
}); });
it('re-decorates with the dirty diff controller', () => { it('re-decorates with the dirty diff controller', () => {
...@@ -100,7 +141,9 @@ describe('Multi-file editor library', () => { ...@@ -100,7 +141,9 @@ describe('Multi-file editor library', () => {
instance.attachModel(model); instance.attachModel(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(
model,
);
}); });
}); });
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import store from 'ee/ide/stores'; import store from 'ee/ide/stores';
import service from 'ee/ide/services'; import service from 'ee/ide/services';
import router from 'ee/ide/ide_router'; import router from 'ee/ide/ide_router';
import eventHub from 'ee/ide/eventhub';
import { file, resetStore } from '../../helpers'; import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => { describe('Multi-file store file actions', () => {
...@@ -295,6 +296,8 @@ describe('Multi-file store file actions', () => { ...@@ -295,6 +296,8 @@ describe('Multi-file store file actions', () => {
let tmpFile; let tmpFile;
beforeEach(() => { beforeEach(() => {
spyOn(eventHub, '$on');
tmpFile = file(); tmpFile = file();
tmpFile.content = 'testing'; tmpFile.content = 'testing';
......
...@@ -291,4 +291,15 @@ describe('Multi-file store actions', () => { ...@@ -291,4 +291,15 @@ describe('Multi-file store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('updateViewer', () => {
it('updates viewer state', (done) => {
store.dispatch('updateViewer', 'diff')
.then(() => {
expect(store.state.viewer).toBe('diff');
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -409,6 +409,8 @@ describe('IDE commit module actions', () => { ...@@ -409,6 +409,8 @@ describe('IDE commit module actions', () => {
}); });
it('redirects to new merge request page', (done) => { it('redirects to new merge request page', (done) => {
spyOn(eventHub, '$on');
store.state.commit.commitAction = '3'; store.state.commit.commitAction = '3';
store.dispatch('commit/commitChanges') store.dispatch('commit/commitChanges')
......
...@@ -68,4 +68,12 @@ describe('Multi-file store mutations', () => { ...@@ -68,4 +68,12 @@ describe('Multi-file store mutations', () => {
expect(localState.rightPanelCollapsed).toBeFalsy(); expect(localState.rightPanelCollapsed).toBeFalsy();
}); });
}); });
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
expect(localState.viewer).toBe('diff');
});
});
}); });
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