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 {
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('peek');
const paddingTop = 16;
this.diffsLoaded = false;
......@@ -86,6 +87,10 @@ export default class MergeRequestTabs {
this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
......
......@@ -132,13 +132,35 @@
.multi-file-tabs {
display: flex;
overflow-x: auto;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
> li {
> ul {
display: flex;
overflow-x: auto;
}
li {
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 {
......@@ -207,6 +229,70 @@
.vertical-center {
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 {
......
......@@ -29,7 +29,7 @@ in your testing/production environment.
GitLab stores a number of secret values in the `/etc/gitlab/gitlab-secrets.json`
file which *must* match between the primary and secondary nodes. Until there is
a means of automatically replicating these between nodes (see issue [gitlab-org/gitlab-ee#3789]),
a means of automatically replicating these between nodes (see issue [gitlab-org/gitlab-ee#3789]),
they must be manually replicated to the secondary.
1. SSH into the **primary** node, and execute the command below:
......@@ -127,7 +127,11 @@ keys must be manually replicated to the secondary node.
1. Restart sshd:
```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
......@@ -145,13 +149,13 @@ keys must be manually replicated to the secondary node.
```
gitlab-ctl restart
```
Check if there are any common issue with your Geo setup by running:
```
gitlab-rake gitlab:geo:check
```
1. SSH into your GitLab **primary** server and login as root to verify the
secondary is reachable or there are any common issue with your Geo setup:
......@@ -164,13 +168,13 @@ replicating missing data from the primary in a process known as **backfill**.
Meanwhile, the primary node will start to notify the secondary of any changes, so
that the secondary can act on those notifications immediately.
Make sure the secondary instance is running and accessible.
Make sure the secondary instance is running and accessible.
You can login to the secondary node with the same credentials as used in the primary.
### Step 4. (Optional) Enabling hashed storage (from GitLab 10.0)
CAUTION: **Warning**:
Hashed storage is in **Beta**. It is not considered production-ready. See
Hashed storage is in **Beta**. It is not considered production-ready. See
[Hashed Storage] for more detail, and for the latest updates, check
infrastructure issue [gitlab-com/infrastructure#2821].
......
......@@ -16,18 +16,26 @@ codequality:
- docker:dind
script:
- 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
- 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
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- 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:
paths: [codeclimate.json]
```
This will create a `codequality` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
The above example will create a `codequality` job in your CI/CD pipeline which
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
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md).
TIP: **Tip:**
Starting with [GitLab Starter][ee] 9.3, this information will
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
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
......
This diff is collapsed.
This diff is collapsed.
......@@ -24,8 +24,11 @@
methods: {
...mapActions([
'discardFileChanges',
'updateViewer',
]),
openFileInEditor(file) {
this.updateViewer('diff');
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 @@
},
},
computed: {
...mapState([
'changedFiles',
'openFiles',
]),
...mapGetters([
'activeFile',
]),
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
......@@ -66,6 +61,8 @@
>
<repo-tabs
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
/>
<repo-editor
class="multi-file-edit-pane-content"
......
......@@ -16,6 +16,8 @@ export default {
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
......@@ -33,6 +35,9 @@ export default {
rightPanelCollapsed() {
this.editor.updateDimensions();
},
viewer() {
this.createEditorInstance();
},
},
beforeDestroy() {
this.editor.dispose();
......@@ -55,6 +60,8 @@ export default {
'setFileLanguage',
'setEditorPosition',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]),
initMonaco() {
if (this.shouldHideEditor) return;
......@@ -63,16 +70,34 @@ export default {
this.getRawFileData(this.file)
.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) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
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() {
if (!this.file) return;
if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.file);
......
......@@ -52,15 +52,23 @@
}
},
methods: {
...mapActions([
'toggleTreeOpen',
]),
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path);
}
router.push(`/project${this.file.url}`);
const delayPromise = this.file.changed
? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
router.push(`/project${this.file.url}`);
});
},
},
};
......
<script>
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default {
components: {
RepoTab,
EditorMode,
},
props: {
files: {
type: Array,
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>
<template>
<ul
class="multi-file-tabs list-unstyled append-bottom-0"
>
<repo-tab
v-for="file in files"
:key="file.key"
:tab="file"
<div class="multi-file-tabs">
<ul
class="list-unstyled append-bottom-0"
ref="tabsScroller"
>
<repo-tab
v-for="tab in files"
:key="tab.key"
:tab="tab"
/>
</ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
@click="updateViewer"
/>
</ul>
</div>
</template>
......@@ -26,6 +26,9 @@ export default class Model {
this.events = new Map();
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);
}
......@@ -75,6 +78,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
import eventHub from 'ee/ide/eventhub';
import Disposable from './disposable';
import Model from './model';
......@@ -25,9 +26,17 @@ export default class ModelManager {
this.models.set(model.path, model);
this.disposable.add(model);
eventHub.$on(`editor.update.model.dispose.${file.path}`, this.removeCachedModel.bind(this, file));
return model;
}
removeCachedModel(file) {
this.models.delete(file.path);
eventHub.$off(`editor.update.model.dispose.${file.path}`, this.removeCachedModel);
}
dispose() {
// dispose of all the models
this.disposable.dispose();
......
......@@ -27,6 +27,8 @@ export default class DecorationsController {
}
decorate(model) {
if (!this.editor.instance) return;
const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || [];
......
......@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
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 {
static create(monaco) {
......@@ -34,19 +41,31 @@ export default class Editor {
createInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.create(domElement, {
...defaultEditorOptions,
})),
(this.dirtyDiffController = new DirtyDiffController(
this.modelManager,
this.decorationsController,
)),
);
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
createDiffInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
this.instance = this.monaco.editor.create(domElement, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
),
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
})),
);
window.addEventListener('resize', this.debouncedUpdate, false);
......@@ -58,25 +77,39 @@ export default class Editor {
}
attachModel(model) {
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
});
return;
}
this.instance.setModel(model.getModel());
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach((key) => {
Object.assign(acc, {
[key]: obj[key](model),
this.instance.updateOptions(
editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach(key => {
Object.assign(acc, {
[key]: obj[key](model),
});
});
});
return acc;
}, {}));
return acc;
}, {}),
);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab');
}
......@@ -88,12 +121,21 @@ export default class Editor {
}
dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
// catch any potential errors with disposing the error
// this is mainly for tests caused by elements not existing
try {
this.disposable.dispose();
this.instance = null;
} catch (e) {
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 {
}
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
......
export default [{
readOnly: model => !!model.file.file_lock,
}];
export const defaultEditorOptions = {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
};
export default [
{
readOnly: model => !!model.file.file_lock,
},
];
......@@ -6,6 +6,9 @@ export default {
rules: [],
colors: {
'editorLineNumber.foreground': '#CCCCCC',
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
},
},
};
......@@ -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/file';
export * from './actions/project';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import eventHub from 'ee/ide/eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
setPageTitle,
} from '../utils';
import { setPageTitle } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.indexOf(path);
......@@ -23,6 +22,8 @@ export const closeFile = ({ commit, state, getters, dispatch }, path) => {
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
};
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 (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 });
......@@ -45,15 +49,16 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file });
return service.getFileData(file.url)
.then((res) => {
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
......@@ -61,15 +66,33 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
});
};
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const getRawFileData = ({ commit, dispatch }, file) =>
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
......@@ -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) {
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) => {
if (file.tempFile && file.opened) {
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 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) => {
const project = state.projects[projectId];
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
const project = state.projects[projectId];
return {
...project,
branches: Object.keys(project.branches).map((branchId) => {
const branch = project.branches[branchId];
return {
...project,
branches: Object.keys(project.branches).map(branchId => {
const branch = project.branches[branchId];
return {
...branch,
tree: state.trees[branch.treeId],
};
}),
};
});
return {
...branch,
tree: state.trees[branch.treeId],
};
}),
};
});
// eslint-disable-next-line no-confusing-arrow
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';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......@@ -11,7 +11,10 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
......@@ -63,8 +66,8 @@ export default {
[key]: entry,
});
} else {
const tree = entry.tree.filter(f =>
foundEntry.tree.find(e => e.path === f.path) === undefined,
const tree = entry.tree.filter(
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree),
......@@ -74,14 +77,28 @@ export default {
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) {
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,
...fileMutations,
...treeMutations,
......
......@@ -14,4 +14,6 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
});
......@@ -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', () => {
spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
......@@ -44,6 +45,16 @@ describe('Multi-file editor commit sidebar list item', () => {
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('iconName', () => {
it('returns modified when not a tempFile', () => {
......
......@@ -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', () => {
it('creates new model', () => {
spyOn(vm.editor, 'createModel').and.callThrough();
......
......@@ -12,9 +12,11 @@ describe('RepoTabs', () => {
vm.$destroy();
});
it('renders a list of tabs', (done) => {
it('renders a list of tabs', done => {
vm = createComponent(RepoTabs, {
files: openedFiles,
viewer: 'editor',
hasChanges: false,
});
openedFiles[0].active = true;
......@@ -28,4 +30,52 @@ describe('RepoTabs', () => {
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 */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader';
import ModelManager from 'ee/ide/lib/common/model_manager';
import { file } from '../../helpers';
......@@ -47,6 +48,15 @@ describe('Multi-file editor library model manager', () => {
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', () => {
......@@ -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', () => {
it('clears cached models', () => {
instance.addModel(file());
......
/* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader';
import Model from 'ee/ide/lib/common/model';
import { file } from '../../helpers';
......@@ -7,6 +8,8 @@ describe('Multi-file editor library model', () => {
let model;
beforeEach((done) => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path'));
......@@ -23,6 +26,10 @@ describe('Multi-file editor library model', () => {
expect(model.model).not.toBeNull();
});
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
describe('path', () => {
it('returns file path', () => {
expect(model.path).toBe('path');
......@@ -88,5 +95,13 @@ describe('Multi-file editor library model', () => {
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';
describe('Multi-file editor library', () => {
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'], () => {
instance = editor.create(monaco);
......@@ -16,6 +24,8 @@ describe('Multi-file editor library', () => {
afterEach(() => {
instance.dispose();
el.remove();
});
it('creates instance of editor', () => {
......@@ -27,33 +37,48 @@ describe('Multi-file editor library', () => {
});
describe('createInstance', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough();
instance.createInstance(el);
instance.createInstance(holder);
expect(instance.monaco.editor.create).toHaveBeenCalled();
});
it('creates dirty diff controller', () => {
instance.createInstance(el);
instance.createInstance(holder);
expect(instance.dirtyDiffController).not.toBeNull();
});
it('creates model manager', () => {
instance.createInstance(el);
instance.createInstance(holder);
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', () => {
it('calls model manager addModel', () => {
spyOn(instance.modelManager, 'addModel');
......@@ -87,12 +112,28 @@ describe('Multi-file editor library', () => {
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', () => {
spyOn(instance.dirtyDiffController, 'attachModel');
instance.attachModel(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(
model,
);
});
it('re-decorates with the dirty diff controller', () => {
......@@ -100,7 +141,9 @@ describe('Multi-file editor library', () => {
instance.attachModel(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(
model,
);
});
});
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import store from 'ee/ide/stores';
import service from 'ee/ide/services';
import router from 'ee/ide/ide_router';
import eventHub from 'ee/ide/eventhub';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
......@@ -295,6 +296,8 @@ describe('Multi-file store file actions', () => {
let tmpFile;
beforeEach(() => {
spyOn(eventHub, '$on');
tmpFile = file();
tmpFile.content = 'testing';
......
......@@ -291,4 +291,15 @@ describe('Multi-file store actions', () => {
.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', () => {
});
it('redirects to new merge request page', (done) => {
spyOn(eventHub, '$on');
store.state.commit.commitAction = '3';
store.dispatch('commit/commitChanges')
......
......@@ -68,4 +68,12 @@ describe('Multi-file store mutations', () => {
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