Commit 4927cb75 authored by Phil Hughes's avatar Phil Hughes

Remove IDE from CE

parent f29dbaf5
<script>
import { mapState } from 'vuex';
import icon from '../../../vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
export default {
components: {
icon,
listItem,
listCollapsed,
},
props: {
title: {
type: String,
required: true,
},
fileList: {
type: Array,
required: true,
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
},
},
};
</script>
<template>
<div class="multi-file-commit-list">
<list-collapsed
v-if="rightPanelCollapsed"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
/>
</li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
},
computed: {
...mapGetters([
'addedFiles',
'modifiedFiles',
]),
},
};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
<icon
name="file-addition"
:size="18"
css-classes="multi-file-addition append-bottom-10"
/>
{{ addedFiles.length }}
<icon
name="file-modified"
:size="18"
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
/>
{{ modifiedFiles.length }}
</div>
</template>
<script>
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
};
</script>
<template>
<div class="multi-file-commit-list-item">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>
<span class="multi-file-commit-list-path">
{{ file.path }}
</span>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'currentBlobView',
'selectedFile',
]),
...mapGetters([
'changedFiles',
'activeFile',
]),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
<div
class="ide-view"
>
<ide-sidebar />
<div
class="multi-file-edit-pane"
>
<template
v-if="activeFile"
>
<repo-tabs/>
<component
class="multi-file-edit-pane-content"
:is="currentBlobView"
/>
<repo-file-buttons />
<ide-status-bar
:file="selectedFile"
/>
</template>
<template
v-else
>
<div class="ide-empty-state">
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
<div class="col-xs-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
</h4>
<p>
You can select a file in the left sidebar to begin
editing and use the right sidebar to commit your changes.
</p>
</div>
</div>
</div>
</div>
</template>
</div>
<ide-contextbar/>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
},
data() {
return {
width: 290,
};
},
computed: {
...mapState([
'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
]),
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.rightPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
>
<div class="multi-file-commit-panel-section">
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section />
</div>
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"
/>
</div>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title">
<icon
name="branch"
:size="12"
/>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""
/>
</div>
</div>
<div>
<repo-tree :tree-id="branch.treeId" />
</div>
</div>
</template>
<script>
import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import branchesTree from './ide_project_branches_tree.vue';
export default {
components: {
branchesTree,
projectAvatarImage,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="projects-sidebar">
<div class="context-header">
<a
:title="project.name"
:href="project.web_url"
>
<div class="avatar-container s40 project-avatar">
<project-avatar-image
class="avatar-container project-avatar"
:link-href="project.path"
:img-src="project.avatar_url"
:img-alt="project.name"
:img-size="40"
/>
</div>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="branch in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"
/>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import repoPreviousDirectory from './repo_prev_directory.vue';
import repoFile from './repo_file.vue';
import { treeList } from '../stores/utils';
export default {
components: {
repoPreviousDirectory,
repoFile,
skeletonLoadingContainer,
},
props: {
treeId: {
type: String,
required: true,
},
},
computed: {
...mapState([
'trees',
'isRoot',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
fetchedList() {
return treeList(this.$store.state, this.treeId);
},
hasPreviousDirectory() {
return !this.isRoot && this.fetchedList.length;
},
showLoading() {
if (this.trees[this.treeId]) {
return this.trees[this.treeId].loading;
}
return true;
},
},
};
</script>
<template>
<div>
<div class="ide-file-list">
<table class="table">
<tbody
v-if="treeId"
>
<repo-previous-directory
v-if="hasPreviousDirectory"
/>
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-for="n in 3"
:key="n"
>
<skeleton-loading-container />
</div>
</template>
<repo-file
v-for="file in fetchedList"
:key="file.key"
:file="file"
/>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue';
export default {
components: {
projectTree,
icon,
panelResizer,
skeletonLoadingContainer,
},
data() {
return {
width: 290,
};
},
computed: {
...mapState([
'loading',
'projects',
'leftPanelCollapsed',
]),
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.leftPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
showLoading() {
return this.loading;
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'left',
collapsed: !this.leftPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': leftPanelCollapsed,
}"
:style="panelStyle"
>
<div class="multi-file-commit-panel-inner">
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-for="n in 3"
:key="n"
>
<skeleton-loading-container />
</div>
</template>
<project-tree
v-for="project in projects"
:key="project.id"
:project="project"
/>
</div>
<button
type="button"
class="btn btn-transparent left-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
<span
v-if="!leftPanelCollapsed"
class="collapse-text"
>
Collapse sidebar
</span>
</button>
<panel-resizer
:size.sync="width"
:enabled="!leftPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="right"
/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
icon,
},
directives: {
tooltip,
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapState([
'selectedFile',
]),
},
};
</script>
<template>
<div class="ide-status-bar">
<div>
<icon
name="branch"
:size="12"
/>
{{ selectedFile.branchId }}
</div>
<div>
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url"
>
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
</a>
</div>
</div>
<div class="text-right">
{{ selectedFile.name }}
</div>
<div class="text-right">
{{ selectedFile.eol }}
</div>
<div class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
{{ selectedFile.fileLanguage }}
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '~/flash';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: {
loadingIcon,
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
...mapState([
'currentBranch',
]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
},
methods: {
...mapActions([
'createNewBranch',
]),
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
submitNewBranch() {
// need to query as the element is appended outside of Vue
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
this.loading = true;
if (flashEl) {
hideFlash(flashEl, false);
}
this.createNewBranch(this.branchName)
.then(() => {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranchId;
}
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
},
},
};
</script>
<template>
<div>
<div
class="flash-container"
ref="flashContainer"
>
</div>
<p>
Create from:
<code>{{ currentBranch }}</code>
</p>
<input
class="form-control js-new-branch-name"
type="text"
placeholder="Name new branch"
v-model="branchName"
@keyup.enter.stop.prevent="submitNewBranch"
/>
<div class="prepend-top-default clearfix">
<button
type="button"
class="btn btn-primary pull-left"
:disabled="btnDisabled"
@click.stop.prevent="submitNewBranch"
>
<loading-icon
v-if="loading"
:inline="true"
/>
<span>Create</span>
</button>
<button
type="button"
class="btn btn-default pull-right"
@click.stop.prevent="toggleDropdown"
>
Cancel
</button>
</div>
</div>
</template>
<script>
import newModal from './modal.vue';
import upload from './upload.vue';
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
newModal,
upload,
},
props: {
branch: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
data() {
return {
openModal: false,
modalType: '',
};
},
methods: {
createNewItem(type) {
this.modalType = type;
this.openModal = true;
},
hideModal() {
this.openModal = false;
},
},
};
</script>
<template>
<div class="repo-new-btn pull-right">
<div class="dropdown">
<button
type="button"
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<icon
name="plus"
:size="12"
css-classes="pull-left"
/>
<icon
name="arrow-down"
:size="12"
css-classes="pull-left"
/>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:branch-id="branch"
:path="path"
:parent="parent"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</div>
<new-modal
v-if="openModal"
:type="modalType"
:branch-id="branch"
:path="path"
:parent="parent"
@hide="hideModal"
/>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
import modal from '../../../vue_shared/components/modal.vue';
export default {
components: {
modal,
},
props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.path !== '' ? `${this.path}/` : '',
};
},
computed: {
...mapState([
'currentProjectId',
]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
this.hideModal();
},
hideModal() {
this.$emit('hide');
},
},
};
</script>
<template>
<modal
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@cancel="hideModal"
@submit="createEntryInStore"
>
<form
class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
</form>
</modal>
</template>
<script>
import { mapActions, mapState } from 'vuex';
export default {
props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
if (!isText) {
result = result.split('base64,')[1];
}
this.createTempEntry({
name,
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
});
},
readFile(file) {
const reader = new FileReader();
const isText = file.type.match(/text.*/) !== null;
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
},
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
startFileUpload() {
this.$refs.fileUpload.click();
},
},
};
</script>
<template>
<div>
<a
href="#"
role="button"
@click.prevent="startFileUpload"
>
{{ __('Upload file') }}
</a>
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import commitFilesList from './commit_sidebar/list.vue';
export default {
components: {
modal,
icon,
commitFilesList,
},
directives: {
tooltip,
},
data() {
return {
showNewBranchModal: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
]),
commitButtonDisabled() {
return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
},
commitMessageCount() {
return this.commitMessage.length;
},
},
methods: {
...mapActions([
'checkCommitStatus',
'commitChanges',
'getTreeData',
'setPanelCollapsedStatus',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
branch: createNewBranch ?
`${this.currentBranchId}-${new Date().getTime().toString()}` :
this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: createNewBranch ? this.currentBranchId : undefined,
};
this.showNewBranchModal = false;
this.submitCommitsLoading = true;
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
this.commitMessage = '';
this.startNewMR = false;
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
tryCommit() {
this.submitCommitsLoading = true;
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
this.showNewBranchModal = true;
} else {
this.makeCommit();
}
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
},
};
</script>
<template>
<div class="multi-file-commit-panel-section">
<modal
v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__(`This branch has changed since
you started editing. Would you like to create a new branch?`)"
@cancel="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
<commit-files-list
title="Staged"
:file-list="changedFiles"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message"
name="commit-message"
v-model="commitMessage"
placeholder="Commit message"
>
</textarea>
</div>
<div class="multi-file-commit-fieldset">
<label
v-tooltip
title="Create a new merge request with these changes"
data-container="body"
data-placement="top"
>
<input
type="checkbox"
v-model="startNewMR"
/>
Merge Request
</label>
<button
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
>
<i
v-if="submitCommitsLoading"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading"
>
</i>
Commit
</button>
<div
class="multi-file-commit-message-count"
>
{{ commitMessageCount }}
</div>
</div>
</form>
</div>
</template>
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import modal from '~/vue_shared/components/modal.vue';
export default {
components: {
modal,
},
computed: {
...mapState([
'editMode',
'discardPopupOpen',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
},
methods: {
...mapActions([
'toggleEditMode',
'closeDiscardPopup',
]),
},
};
</script>
<template>
<div class="editable-mode">
<button
v-if="canEditFile"
class="btn btn-default"
type="button"
@click.prevent="toggleEditMode()">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{ buttonLabel }}
</span>
</button>
<modal
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@cancel="closeDiscardPopup"
@submit="toggleEditMode(true)"
/>
</div>
</template>
<script>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default {
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
this.initMonaco();
}
},
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
},
beforeDestroy() {
this.editor.dispose();
},
mounted() {
if (this.editor && monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
this.editor = Editor.create(monaco);
this.initMonaco();
});
}
},
methods: {
...mapActions([
'getRawFileData',
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileEOL',
]),
initMonaco() {
if (this.shouldHideEditor) return;
this.editor.clearEditor();
this.getRawFileData(this.activeFile)
.then(() => {
this.editor.createInstance(this.$refs.editor);
})
.then(() => this.setupEditor())
.catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
setupEditor() {
if (!this.activeFile) return;
const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model);
model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
content: m.getValue(),
});
});
// Handle Cursor Position
this.editor.onPositionChange((instance, e) => {
this.setEditorPosition({
editorRow: e.position.lineNumber,
editorColumn: e.position.column,
});
});
this.editor.setPosition({
lineNumber: this.activeFile.editorRow,
column: this.activeFile.editorColumn,
});
// Handle File Language
this.setFileLanguage({
fileLanguage: model.language,
});
// Get File eol
this.setFileEOL({
eol: model.eol,
});
},
},
};
</script>
<template>
<div
id="ide"
class="blob-viewer-container blob-editor-container"
>
<div
v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
<div
v-show="!shouldHideEditor"
ref="editor"
class="multi-file-editor-holder"
>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
skeletonLoadingContainer,
newDropdown,
fileIcon,
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
showExtraColumns: {
type: Boolean,
default: false,
},
},
computed: {
...mapState([
'leftPanelCollapsed',
]),
isSubmodule() {
return this.file.type === 'submodule';
},
isTree() {
return this.file.type === 'tree';
},
levelIndentation() {
if (this.file.level > 0) {
return {
marginLeft: `${this.file.level * 16}px`,
};
}
return {};
},
shortId() {
return this.file.id.substr(0, 8);
},
submoduleColSpan() {
return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
},
fileClass() {
if (this.file.type === 'blob') {
if (this.file.active) {
return 'file-open file-active';
}
return this.file.opened ? 'file-open' : '';
}
return '';
},
changedClass() {
return {
'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
};
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
methods: {
clickFile(row) {
// Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
this.$store.dispatch('toggleTreeOpen', {
endpoint: this.file.url,
tree: this.file,
});
}
this.$router.push(`/project${row.url}`);
},
},
};
</script>
<template>
<tr
class="file"
:class="fileClass"
@click="clickFile(file)">
<td
class="multi-file-table-name"
:colspan="submoduleColSpan"
>
<a
class="repo-file-name"
>
<file-icon
:file-name="file.name"
:loading="file.loading"
:folder="file.type === 'tree'"
:opened="file.opened"
:style="levelIndentation"
:size="16"
/>
{{ file.name }}
</a>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
:parent="file"
/>
<i
class="fa"
v-if="file.changed || file.tempFile"
:class="changedClass"
aria-hidden="true"
>
</i>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@click.stop
:href="file.tree_url"
>
{{ shortId }}
</a>
</span>
</template>
</td>
<template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"
@click.stop
:href="file.lastCommit.url"
>
{{ file.lastCommit.message }}
</a>
<skeleton-loading-container
v-else
:small="true"
/>
</td>
<td class="commit-update hidden-xs text-right">
<span
v-if="file.lastCommit.updatedAt"
:title="tooltipTitle(file.lastCommit.updatedAt)"
>
{{ timeFormated(file.lastCommit.updatedAt) }}
</span>
<skeleton-loading-container
v-else
class="animation-container-right"
:small="true"
/>
</td>
</template>
</tr>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() {
return this.activeFile.binary ? 'Download' : 'Raw';
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="multi-file-editor-btn-group"
>
<a
:href="activeFile.rawPath"
target="_blank"
class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
{{ rawDownloadButtonLabel }}
</a>
<div
class="btn-group"
role="group"
aria-label="File actions"
>
<a
:href="activeFile.blamePath"
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="activeFile.commitsPath"
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="activeFile.permalink"
class="btn btn-default btn-sm permalink"
>
Permalink
</a>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
export default {
components: {
skeletonLoadingContainer,
},
computed: {
...mapState([
'leftPanelCollapsed',
]),
},
};
</script>
<template>
<tr
class="loading-file"
aria-label="Loading files"
>
<td class="multi-file-table-col-name">
<skeleton-loading-container
:small="true"
/>
</td>
<template v-if="!leftPanelCollapsed">
<td class="hidden-sm hidden-xs">
<skeleton-loading-container
:small="true"
/>
</td>
<td class="hidden-xs">
<skeleton-loading-container
class="animation-container-right"
:small="true"
/>
</td>
</template>
</tr>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState([
'parentTreeUrl',
'leftPanelCollapsed',
]),
colSpanCondition() {
return this.leftPanelCollapsed ? undefined : 3;
},
},
methods: {
...mapActions([
'getTreeData',
]),
},
};
</script>
<template>
<tr class="file prev-directory">
<td
:colspan="colSpanCondition"
class="table-cell"
@click.prevent="getTreeData({ endpoint: parentTreeUrl })"
>
<a :href="parentTreeUrl">...</a>
</td>
</tr>
</template>
<script>
import { mapGetters } from 'vuex';
import LineHighlighter from '~/line_highlighter';
import syntaxHighlight from '~/syntax_highlight';
export default {
computed: {
...mapGetters([
'activeFile',
]),
renderErrorTooLarge() {
return this.activeFile.renderError === 'too_large';
},
},
mounted() {
this.highlightFile();
this.lineHighlighter = new LineHighlighter({
fileHolderSelector: '.blob-viewer-container',
scrollFileHolder: true,
});
},
updated() {
this.$nextTick(() => {
this.highlightFile();
});
},
methods: {
highlightFile() {
syntaxHighlight($(this.$el).find('.file-content'));
},
},
};
</script>
<template>
<div>
<div
v-if="!activeFile.renderError"
v-html="activeFile.html"
class="multi-file-preview-holder"
>
</div>
<div
v-else-if="activeFile.tempFile"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed for this temporary file.
</p>
</div>
<div
v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large.
You can <a
:href="activeFile.rawPath"
download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occurred.
You can <a
:href="activeFile.rawPath"
download>download</a> it instead.
</p>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import fileIcon from '~/vue_shared/components/file_icon.vue';
export default {
components: {
fileIcon,
},
props: {
tab: {
type: Object,
required: true,
},
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() {
const tabChangedObj = {
'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
};
return tabChangedObj;
},
},
methods: {
...mapActions([
'closeFile',
]),
clickFile(tab) {
this.$router.push(`/project${tab.url}`);
},
},
};
</script>
<template>
<li @click="clickFile(tab)">
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel"
:class="{
'modified': tab.changed,
}"
:disabled="tab.changed"
>
<i
class="fa"
:class="changedClass"
aria-hidden="true"
>
</i>
</button>
<div
class="multi-file-tab"
:class="{active : tab.active }"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
</div>
</li>
</template>
<script>
import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
export default {
components: {
'repo-tab': RepoTab,
},
computed: {
...mapState([
'openFiles',
]),
},
};
</script>
<template>
<ul
class="multi-file-tabs list-unstyled append-bottom-0"
>
<repo-tab
v-for="tab in openFiles"
:key="tab.key"
:tab="tab"
/>
</ul>
</template>
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from './stores';
import flash from '../flash';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter);
/**
* Routes below /-/ide/:
/project/h5bp/html5-boilerplate/blob/master
/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
/project/h5bp/html5-boilerplate/mr/123
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
/workspace/123
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
/workspace/project/h5bp/html5-boilerplate/mr/123
/ = /workspace
/settings
*/
// Unfortunately Vue Router doesn't work without at least a fake component
// If you do only data handling
const EmptyRouterComponent = {
render(createElement) {
return createElement('div');
},
};
const router = new VueRouter({
mode: 'history',
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
path: '/project/:namespace/:project',
component: EmptyRouterComponent,
children: [
{
path: ':targetmode/:branch/*',
component: EmptyRouterComponent,
},
{
path: 'mr/:mrid',
component: EmptyRouterComponent,
},
],
},
],
});
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
store.dispatch('getProjectData', {
namespace: to.params.namespace,
projectId: to.params.project,
})
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
});
store.dispatch('getTreeData', {
projectId: fullProjectId,
branch: to.params.branch,
endpoint: `/tree/${to.params.branch}`,
})
.then(() => {
if (to.params[0]) {
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
}
})
.catch((e) => {
flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
})
.catch((e) => {
flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
next();
});
export default router;
import Vue from 'vue';
import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
import Translate from '../vue_shared/translate';
function initIde(el) {
if (!el) return null;
return new Vue({
el,
store,
router,
components: {
ide,
},
render(createElement) {
return createElement('ide', {
props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
},
});
},
});
}
const ideElement = document.getElementById('ide');
Vue.use(Translate);
initIde(ideElement);
export default class Disposable {
constructor() {
this.disposers = new Set();
}
add(...disposers) {
disposers.forEach(disposer => this.disposers.add(disposer));
}
dispose() {
this.disposers.forEach(disposer => disposer.dispose());
this.disposers.clear();
}
}
/* global monaco */
import Disposable from './disposable';
export default class Model {
constructor(monaco, file) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
this.originalModel = this.monaco.editor.createModel(
this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.path}`),
),
this.model = this.monaco.editor.createModel(
this.content,
undefined,
new this.monaco.Uri(null, null, this.file.path),
),
);
this.events = new Map();
}
get url() {
return this.model.uri.toString();
}
get language() {
return this.model.getModeId();
}
get eol() {
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
}
get path() {
return this.file.path;
}
getModel() {
return this.model;
}
getOriginalModel() {
return this.originalModel;
}
onChange(cb) {
this.events.set(
this.path,
this.disposable.add(
this.model.onDidChangeContent(e => cb(this.model, e)),
),
);
}
dispose() {
this.disposable.dispose();
this.events.clear();
}
}
import Disposable from './disposable';
import Model from './model';
export default class ModelManager {
constructor(monaco) {
this.monaco = monaco;
this.disposable = new Disposable();
this.models = new Map();
}
hasCachedModel(path) {
return this.models.has(path);
}
addModel(file) {
if (this.hasCachedModel(file.path)) {
return this.models.get(file.path);
}
const model = new Model(this.monaco, file);
this.models.set(model.path, model);
this.disposable.add(model);
return model;
}
dispose() {
// dispose of all the models
this.disposable.dispose();
this.models.clear();
}
}
export default class DecorationsController {
constructor(editor) {
this.editor = editor;
this.decorations = new Map();
this.editorDecorations = new Map();
}
getAllDecorationsForModel(model) {
if (!this.decorations.has(model.url)) return [];
const modelDecorations = this.decorations.get(model.url);
const decorations = [];
modelDecorations.forEach(val => decorations.push(...val));
return decorations;
}
addDecorations(model, decorationsKey, decorations) {
const decorationMap = this.decorations.get(model.url) || new Map();
decorationMap.set(decorationsKey, decorations);
this.decorations.set(model.url, decorationMap);
this.decorate(model);
}
decorate(model) {
const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || [];
this.editorDecorations.set(
model.url,
this.editor.instance.deltaDecorations(oldDecorations, decorations),
);
}
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
}
}
/* global monaco */
import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
export const getDiffChangeType = (change) => {
if (change.modified) {
return 'modified';
} else if (change.added) {
return 'added';
} else if (change.removed) {
return 'removed';
}
return '';
};
export const getDecorator = change => ({
range: new monaco.Range(
change.lineNumber,
1,
change.endLineNumber,
1,
),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
},
});
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
this.dirtyDiffWorker = new DirtyDiffWorker();
this.throttledComputeDiff = throttle(this.computeDiff, 250);
this.decorate = this.decorate.bind(this);
this.dirtyDiffWorker.addEventListener('message', this.decorate);
}
attachModel(model) {
model.onChange(() => this.throttledComputeDiff(model));
}
computeDiff(model) {
this.dirtyDiffWorker.postMessage({
path: model.path,
originalContent: model.getOriginalModel().getValue(),
newContent: model.getModel().getValue(),
});
}
reDecorate(model) {
this.decorationsController.decorate(model);
}
decorate({ data }) {
const decorations = data.changes.map(change => getDecorator(change));
this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
}
dispose() {
this.disposable.dispose();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
}
}
import { diffLines } from 'diff';
// eslint-disable-next-line import/prefer-default-export
export const computeDiff = (originalContent, newContent) => {
const changes = diffLines(originalContent, newContent);
let lineNumber = 1;
return changes.reduce((acc, change) => {
const findOnLine = acc.find(c => c.lineNumber === lineNumber);
if (findOnLine) {
Object.assign(findOnLine, change, {
modified: true,
endLineNumber: (lineNumber + change.count) - 1,
});
} else if ('added' in change || 'removed' in change) {
acc.push(Object.assign({}, change, {
lineNumber,
modified: undefined,
endLineNumber: (lineNumber + change.count) - 1,
}));
}
if (!change.removed) {
lineNumber += change.count;
}
return acc;
}, []);
};
import { computeDiff } from './diff';
self.addEventListener('message', (e) => {
const data = e.data;
self.postMessage({
path: data.path,
changes: computeDiff(data.originalContent, data.newContent),
});
});
import _ from 'underscore';
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';
export default class Editor {
static create(monaco) {
this.editorInstance = new Editor(monaco);
return this.editorInstance;
}
constructor(monaco) {
this.monaco = monaco;
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
this.disposable.add(
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
}, 200);
window.addEventListener('resize', this.debouncedUpdate, false);
}
createInstance(domElement) {
if (!this.instance) {
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,
),
);
}
}
createModel(file) {
return this.modelManager.addModel(file);
}
attachModel(model) {
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),
});
});
return acc;
}, {}));
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
}
}
dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
updateDimensions() {
this.instance.layout();
}
setPosition({ lineNumber, column }) {
this.instance.revealPositionInCenter({
lineNumber,
column,
});
this.instance.setPosition({
lineNumber,
column,
});
}
onPositionChange(cb) {
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
}
}
import monacoContext from 'monaco-editor/dev/vs/loader';
monacoContext.require.config({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
},
});
// ignore CDN config and use local assets path for service worker which cannot be cross-domain
const relativeRootPath = (gon && gon.relative_url_root) || '';
const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
// eslint-disable-next-line no-underscore-dangle
window.__monaco_context__ = monacoContext;
export default monacoContext.require;
import Vue from 'vue';
import VueResource from 'vue-resource';
import Api from '../../api';
Vue.use(VueResource);
export default {
getTreeData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getFileData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getRawFileData(file) {
if (file.tempFile) {
return Promise.resolve(file.content);
}
if (file.raw) {
return Promise.resolve(file.raw);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
return Vue.http.post(url, payload);
},
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
getTreeLastCommit(endpoint) {
return Vue.http.get(endpoint, {
params: {
format: 'json',
},
});
},
};
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import service from '../services';
import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) =>
commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, getters, dispatch }) => {
const changedFiles = getters.changedFiles;
changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
if (file.tempFile) {
dispatch('closeFile', { file, force: true });
}
});
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', { file }));
};
export const toggleEditMode = (
{ state, commit, getters, dispatch },
force = false,
) => {
const changedFiles = getters.changedFiles;
if (changedFiles.length && !force) {
commit(types.TOGGLE_DISCARD_POPUP, true);
} else {
commit(types.TOGGLE_EDIT_MODE);
commit(types.TOGGLE_DISCARD_POPUP, false);
dispatch('toggleBlobView');
if (!state.editMode) {
dispatch('discardAllChanges');
}
}
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
if (side === 'left') {
commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
} else {
commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
}
};
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
export const checkCommitStatus = ({ state }) =>
service
.getBranchData(state.currentProjectId, state.currentBranchId)
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
state.projects[state.currentProjectId].branches[state.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = (
{ commit, state, dispatch, getters },
{ payload, newMr },
) =>
service
.commit(state.currentProjectId, payload)
.then(({ data }) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
return;
}
const selectedProject = state.projects[state.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
commit: {
message: data.message,
authored_date: data.committed_date,
},
};
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
flash(
commitMsg,
'notice',
document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) {
dispatch('discardAllChanges');
dispatch(
'redirectToUrl',
`${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
projectId: state.currentProjectId,
branchId: state.currentBranchId,
reference: data.id,
});
getters.changedFiles.forEach((entry) => {
commit(types.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
});
});
dispatch('discardAllChanges');
window.scrollTo(0, 0);
}
})
.catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
) => {
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
if (type === 'tree') {
dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
});
} else if (type === 'blob') {
dispatch('createTempFile', {
projectId,
branchId,
parent: selectedParent,
name,
base64,
content,
});
}
};
export const scrollToTab = () => {
Vue.nextTick(() => {
const tabs = document.getElementById('tabs');
if (tabs) {
const tabEl = tabs.querySelector('.active .repo-tab');
tabEl.focus();
}
});
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/branch';
import service from '../../services';
import flash from '../../../flash';
import * as types from '../mutation_types';
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.currentProjectId,
{
branch,
ref: state.currentBranchId,
},
)
.then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranchId, branchName);
if (this.$router) this.$router.push(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
findEntry,
setPageTitle,
createTemp,
findIndexOfFile,
} from '../utils';
export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
if ((file.changed || file.tempFile) && !force) return;
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.openFiles[nextIndexToOpen];
dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
const currentActiveFile = getters.activeFile;
if (file.active) return;
if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
}
commit(types.SET_FILE_ACTIVE, { file, active: true });
dispatch('scrollToTab');
// reset hash for line highlighting
location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, file);
service.getFileData(file.url)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
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 changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
}
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
const path = parent.path !== undefined ? parent.path : '';
// We need to do the replacement otherwise the web_url + file.url duplicate
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
projectId,
branchId,
name: name.replace(`${path}/`, ''),
path,
type: 'blob',
level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
url: newUrl,
});
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, {
parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
if (!state.editMode && !file.base64) {
dispatch('toggleEditMode', true);
}
router.push(`/project${file.url}`);
return Promise.resolve(file);
};
import service from '../../services';
import flash from '../../../flash';
import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, state);
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
commit(types.TOGGLE_LOADING, state);
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
import { visitUrl } from '../../../lib/utils/url_utility';
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
setPageTitle,
findEntry,
createTemp,
createOrMergeEntry,
} from '../utils';
export const getTreeData = (
{ commit, state, dispatch },
{ endpoint, tree = null, projectId, branch, force = false } = {},
) => new Promise((resolve, reject) => {
// We already have the base tree so we resolve immediately
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
resolve();
} else {
if (tree) commit(types.TOGGLE_LOADING, tree);
const selectedProject = state.projects[projectId];
// We are merging the web_url that we got on the project info with the endpoint
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
if (completeEndpoint && (!tree || !tree.tempFile)) {
service.getTreeData(completeEndpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
dispatch('updateDirectoryData', { data, tree, projectId, branch });
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
if (tree) commit(types.TOGGLE_LOADING, selectedTree);
const prevLastCommitPath = selectedTree.lastCommitPath;
if (prevLastCommitPath !== null) {
dispatch('getLastCommitData', selectedTree);
}
resolve(data);
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e);
});
} else {
resolve();
}
}
});
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) {
// send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] };
dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
} else {
dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
}
commit(types.TOGGLE_TREE_OPEN, tree);
};
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', {
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row);
visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row);
} else {
dispatch('getFileData', row);
}
};
export const createTempTree = (
{ state, commit, dispatch },
{ projectId, branchId, parent, name },
) => {
let selectedTree = parent;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => {
const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
if (!foundEntry) {
const path = selectedTree.path !== undefined ? selectedTree.path : '';
const tmpEntry = createTemp({
projectId,
branchId,
name: dirName,
path,
type: 'tree',
level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
tree: [],
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
});
commit(types.CREATE_TMP_TREE, {
parent: selectedTree,
tmpEntry,
});
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
router.push(`/project${tmpEntry.url}`);
selectedTree = tmpEntry;
} else {
selectedTree = foundEntry;
}
});
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
}
});
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
{ commit, state },
{ data, tree, projectId, branch },
) => {
if (!tree) {
const existingTree = state.trees[`${projectId}/${branch}`];
if (!existingTree) {
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
}
}
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({
tree: selectedTree,
projectId: `${projectId}`,
branchId: branch,
entry,
level,
type,
parentTreeUrl,
});
const formattedData = [
...data.trees.map(t => createEntry(t, 'tree')),
...data.submodules.map(m => createEntry(m, 'submodule')),
...data.blobs.map(b => createEntry(b, 'blob')),
];
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
};
export const changedFiles = state => state.openFiles.filter(file => file.changed);
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const activeFileExtension = (state) => {
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
return state.canCommit &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: state(),
actions,
mutations,
getters,
});
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
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_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
// Viewer mutation types
export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
export default {
[types.SET_INITIAL_DATA](state, data) {
Object.assign(state, data);
},
[types.SET_PREVIEW_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-preview',
});
},
[types.SET_EDIT_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-editor',
});
},
[types.TOGGLE_LOADING](state, entry) {
Object.assign(entry, {
loading: !entry.loading,
});
},
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
},
[types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
Object.assign(state, {
discardPopupOpen,
});
},
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
leftPanelCollapsed: collapsed,
});
},
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
rightPanelCollapsed: collapsed,
});
},
[types.SET_RESIZING_STATUS](state, resizing) {
Object.assign(state, {
panelResizing: resizing,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
});
},
...projectMutations,
...fileMutations,
...treeMutations,
...branchMutations,
};
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranchId) {
Object.assign(state, {
currentBranchId,
});
},
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
// Add client side properties
Object.assign(branch, {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
});
Object.assign(state.projects[projectPath], {
branches: {
[branchName]: branch,
},
});
},
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference,
});
},
};
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
Object.assign(file, {
active,
});
Object.assign(state, {
selectedFile: file,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
opened: !file.opened,
});
if (file.opened) {
state.openFiles.push(file);
} else {
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
}
},
[types.SET_FILE_DATA](state, { data, file }) {
Object.assign(file, {
blamePath: data.blame_path,
commitsPath: data.commits_path,
permalink: data.permalink,
rawPath: data.raw_path,
binary: data.binary,
html: data.html,
renderError: data.render_error,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
Object.assign(file, {
content,
changed,
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, {
fileLanguage,
});
},
[types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, {
eol,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, {
editorRow,
editorColumn,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: file.raw,
changed: false,
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
},
};
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_PROJECT](state, currentProjectId) {
Object.assign(state, {
currentProjectId,
});
},
[types.SET_PROJECT](state, { projectPath, project }) {
// Add client side properties
Object.assign(project, {
tree: [],
branches: {},
active: true,
});
Object.assign(state, {
projects: Object.assign({}, state.projects, {
[projectPath]: project,
}),
});
},
};
import * as types from '../mutation_types';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
Object.assign(tree, {
opened: !tree.opened,
});
},
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
[treePath]: {
tree: [],
},
}),
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
});
},
[types.SET_PARENT_TREE_URL](state, url) {
Object.assign(state, {
parentTreeUrl: url,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
Object.assign(tree, {
lastCommitPath: url,
});
},
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
};
export default () => ({
canCommit: false,
currentProjectId: '',
currentBranchId: '',
currentBlobView: 'repo-editor',
discardPopupOpen: false,
editMode: true,
endpoints: {},
isRoot: false,
isInitialRoot: false,
lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],
selectedFile: null,
path: '',
parentTreeUrl: '',
trees: {},
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
panelResizing: false,
});
import _ from 'underscore';
export const dataStructure = () => ({
id: '',
key: '',
type: '',
projectId: '',
branchId: '',
name: '',
url: '',
path: '',
level: 0,
tempFile: false,
icon: '',
tree: [],
loading: false,
opened: false,
active: false,
changed: false,
lastCommitPath: '',
lastCommit: {
id: '',
url: '',
message: '',
updatedAt: '',
author: '',
},
tree_url: '',
blamePath: '',
commitsPath: '',
permalink: '',
rawPath: '',
binary: false,
html: '',
raw: '',
content: '',
parentTreeUrl: '',
renderError: false,
base64: false,
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
eol: '',
});
export const decorateData = (entity) => {
const {
id,
projectId,
branchId,
type,
url,
name,
icon,
tree_url,
path,
renderError,
content = '',
tempFile = false,
active = false,
opened = false,
changed = false,
parentTreeUrl = '',
level = 0,
base64 = false,
} = entity;
return {
...dataStructure(),
id,
projectId,
branchId,
key: `${name}-${type}-${id}`,
type,
name,
url,
tree_url,
path,
level,
tempFile,
icon: `fa-${icon}`,
opened,
active,
parentTreeUrl,
changed,
renderError,
content,
base64,
};
};
/*
Takes the multi-dimensional tree and returns a flattened array.
This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state, treeId) => {
const baseTree = state.trees[treeId];
if (baseTree) {
const mapTree = arr => (!arr.tree || !arr.tree.length ?
[] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(baseTree.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
}
return [];
};
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
export const getTreeEntry = (store, treeId, path) => {
const fileList = treeList(store.state, treeId);
return fileList ? fileList.find(file => file.path === path) : null;
};
export const findEntry = (tree, type, name) => tree.find(
f => f.type === type && f.name === name,
);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
export const createTemp = ({
projectId, branchId, name, path, type, level, changed, content, base64, url,
}) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
projectId,
branchId,
name,
type,
tempFile: true,
path: treePath,
icon: type === 'tree' ? 'folder' : 'file-text-o',
changed,
content,
parentTreeUrl: '',
level,
base64,
renderError: base64,
url,
});
};
export const createOrMergeEntry = ({ tree,
projectId,
branchId,
entry,
type,
parentTreeUrl,
level }) => {
const found = findEntry(tree.tree || tree, type, entry.name);
if (found) {
return Object.assign({}, found, {
id: entry.id,
url: entry.url,
tempFile: false,
});
}
return decorateData({
...entry,
projectId,
branchId,
type,
parentTreeUrl,
level,
});
};
class IdeController < ApplicationController
layout 'nav_only'
def index
end
end
- @body_class = 'ide'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide', force_same_domain: true
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
...@@ -43,8 +43,6 @@ Rails.application.routes.draw do ...@@ -43,8 +43,6 @@ Rails.application.routes.draw do
get 'liveness' => 'health#liveness' get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness' get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check' post 'storage_check' => 'health#storage_check'
get 'ide' => 'ide#index'
get 'ide/*vueroute' => 'ide#index', format: false
resources :metrics, only: [:index] resources :metrics, only: [:index]
mount Peek::Railtie => '/peek' mount Peek::Railtie => '/peek'
......
...@@ -53,7 +53,6 @@ function generateEntries() { ...@@ -53,7 +53,6 @@ function generateEntries() {
common_vue: './vue_shared/vue_resource_interceptor.js', common_vue: './vue_shared/vue_resource_interceptor.js',
locale: './locale/index.js', locale: './locale/index.js',
main: './main.js', main: './main.js',
ide: './ide/index.js',
raven: './raven/index.js', raven: './raven/index.js',
test: './test.js', test: './test.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
......
require 'spec_helper'
feature 'Multi-file editor new directory', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
project.add_master(user)
sign_in(user)
set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
wait_for_requests
click_link('Web IDE')
wait_for_requests
end
after do
set_cookie('new_repo', 'false')
end
it 'creates directory in current directory' do
find('.add-to-tree').click
click_link('New directory')
page.within('.modal') do
find('.form-control').set('folder name')
click_button('Create directory')
end
find('.add-to-tree').click
click_link('New file')
page.within('.modal-dialog') do
find('.form-control').set('file name')
click_button('Create file')
end
wait_for_requests
find('.multi-file-commit-panel-collapse-btn').click
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
expect(page).to have_content('folder name')
end
end
require 'spec_helper'
feature 'Multi-file editor new file', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
project.add_master(user)
sign_in(user)
set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
wait_for_requests
click_link('Web IDE')
wait_for_requests
end
after do
set_cookie('new_repo', 'false')
end
it 'creates file in current directory' do
find('.add-to-tree').click
click_link('New file')
page.within('.modal') do
find('.form-control').set('file name')
click_button('Create file')
end
wait_for_requests
find('.multi-file-commit-panel-collapse-btn').click
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
expect(page).to have_content('file name')
end
end
require 'spec_helper'
feature 'Multi-file editor upload file', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
before do
project.add_master(user)
sign_in(user)
set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
wait_for_requests
click_link('Web IDE')
wait_for_requests
end
after do
set_cookie('new_repo', 'false')
end
it 'uploads text file' do
find('.add-to-tree').click
# make the field visible so capybara can use it
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
attach_file('file-upload', txt_file)
find('.add-to-tree').click
expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
end
it 'uploads image file' do
find('.add-to-tree').click
# make the field visible so capybara can use it
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
attach_file('file-upload', img_file)
find('.add-to-tree').click
expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
expect(page).not_to have_selector('.monaco-editor')
end
end
import Vue from 'vue';
import store from '~/ide/stores';
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list collapsed', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(listCollapsed);
vm = createComponentWithStore(Component, store);
vm.$store.state.openFiles.push(file('file1'), file('file2'));
vm.$store.state.openFiles[0].tempFile = true;
vm.$store.state.openFiles.forEach((f) => {
Object.assign(f, {
changed: true,
});
});
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
it('renders added & modified files count', () => {
expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
});
});
import Vue from 'vue';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
let vm;
let f;
beforeEach(() => {
const Component = Vue.extend(listItem);
f = file('test-file');
vm = mountComponent(Component, {
file: f,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders file path', () => {
expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
});
describe('computed', () => {
describe('iconName', () => {
it('returns modified when not a tempFile', () => {
expect(vm.iconName).toBe('file-modified');
});
it('returns addition when not a tempFile', () => {
f.tempFile = true;
expect(vm.iconName).toBe('file-addition');
});
});
describe('iconClass', () => {
it('returns modified when not a tempFile', () => {
expect(vm.iconClass).toContain('multi-file-modified');
});
it('returns addition when not a tempFile', () => {
f.tempFile = true;
expect(vm.iconClass).toContain('multi-file-addition');
});
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(commitSidebarList);
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
});
vm.$store.state.rightPanelCollapsed = false;
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('empty file list', () => {
it('renders no changes text', () => {
expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes');
});
});
describe('with a list of files', () => {
beforeEach((done) => {
const f = file('file name');
f.changed = true;
vm.fileList.push(f);
Vue.nextTick(done);
});
it('renders list', () => {
expect(vm.$el.querySelectorAll('li').length).toBe(1);
});
});
describe('collapsed', () => {
beforeEach((done) => {
vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done);
});
it('hides list', () => {
expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import ideContextBar from '~/ide/components/ide_context_bar.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Multi-file editor right context bar', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(ideContextBar);
vm = createComponentWithStore(Component, store);
vm.$store.state.rightPanelCollapsed = false;
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('collapsed', () => {
beforeEach((done) => {
vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done);
});
it('adds collapsed class', () => {
expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
});
it('shows correct icon', () => {
expect(vm.currentIcon).toBe('angle-double-left');
});
});
it('clicking toggle collapse button collapses the bar', () => {
spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve());
vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({
side: 'right',
collapsed: true,
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
import { file, resetStore } from '../helpers';
describe('IdeRepoTree', () => {
let vm;
beforeEach(() => {
const IdeRepoTree = Vue.extend(ideRepoTree);
vm = new IdeRepoTree({
store,
propsData: {
treeId: 'abcproject/mybranch',
},
});
vm.$store.state.currentBranch = 'master';
vm.$store.state.isRoot = true;
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a sidebar', () => {
const tbody = vm.$el.querySelector('tbody');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy();
expect(tbody.querySelector('.file')).toBeTruthy();
});
it('renders 3 loading files if tree is loading', (done) => {
vm.treeId = '123';
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3);
done();
});
});
it('renders a prev directory if is not root', (done) => {
vm.$store.state.isRoot = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
done();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
describe('IdeSidebar', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(ideSidebar);
vm = createComponentWithStore(Component, store).$mount();
vm.$store.state.leftPanelCollapsed = false;
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a sidebar', () => {
expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
});
describe('collapsed', () => {
beforeEach((done) => {
vm.$store.state.leftPanelCollapsed = true;
Vue.nextTick(done);
});
it('adds collapsed class', () => {
expect(vm.$el.classList).toContain('is-collapsed');
});
it('shows correct icon', () => {
expect(vm.currentIcon).toBe('angle-double-right');
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
describe('ide component', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(ide);
vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
}).$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('does not render panel right when no files open', () => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
});
it('renders panel right when files are open', (done) => {
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
Vue.nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
done();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import newBranchForm from '~/ide/components/new_branch_form.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
describe('Multi-file editor new branch form', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(newBranchForm);
vm = createComponentWithStore(Component, store);
vm.$store.state.currentBranch = 'master';
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('template', () => {
it('renders submit as disabled', () => {
expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled');
});
it('enables the submit button when branch is not empty', (done) => {
vm.branchName = 'testing';
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull();
done();
});
});
it('displays current branch creating from', (done) => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master');
done();
});
});
});
describe('submitNewBranch', () => {
beforeEach(() => {
spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve());
});
it('sets to loading', () => {
vm.submitNewBranch();
expect(vm.loading).toBeTruthy();
});
it('hides current flash element', (done) => {
vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>';
vm.submitNewBranch();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.flash-alert')).toBeNull();
done();
});
});
it('calls createdNewBranch with branchName', () => {
vm.branchName = 'testing';
vm.submitNewBranch();
expect(vm.createNewBranch).toHaveBeenCalledWith('testing');
});
});
describe('submitNewBranch with error', () => {
beforeEach(() => {
spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({
json: () => Promise.resolve({
message: 'error message',
}),
}));
});
it('sets loading to false', (done) => {
vm.loading = true;
vm.submitNewBranch();
setTimeout(() => {
expect(vm.loading).toBeFalsy();
done();
});
});
it('creates flash element', (done) => {
vm.submitNewBranch();
setTimeout(() => {
expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
done();
});
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('new dropdown component', () => {
let vm;
beforeEach(() => {
const component = Vue.extend(newDropdown);
vm = createComponentWithStore(component, store, {
branch: 'master',
path: '',
});
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.path = '';
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
it('sets modalType to blob when new file is clicked', () => {
vm.$el.querySelectorAll('a')[0].click();
expect(vm.modalType).toBe('blob');
});
it('sets modalType to tree when new directory is clicked', () => {
vm.$el.querySelectorAll('a')[2].click();
expect(vm.modalType).toBe('tree');
});
it('opens modal when link is clicked', (done) => {
vm.$el.querySelectorAll('a')[0].click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.modal')).not.toBeNull();
done();
});
});
});
describe('hideModal', () => {
beforeAll((done) => {
vm.openModal = true;
Vue.nextTick(done);
});
it('closes modal after toggling', (done) => {
vm.hideModal();
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.modal')).toBeNull();
})
.then(done)
.catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import modal from '~/ide/components/new_dropdown/modal.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
let vm;
let projectTree;
beforeEach(() => {
spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
data: {
id: '123',
},
}));
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
data: {
commit: {
id: '123branch',
},
},
}));
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
store.state.projects.abcproject = {
web_url: '',
};
store.state.trees = [];
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
store.state.currentProjectId = 'abcproject';
vm = createComponentWithStore(Component, store, {
type,
branchId: 'master',
path: '',
parent: projectTree,
});
vm.entryName = 'testing';
vm.$mount();
});
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
const title = type === 'tree' ? 'Directory' : 'File';
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
});
describe('createEntryInStore', () => {
it('calls createTempEntry', () => {
spyOn(vm, 'createTempEntry');
vm.createEntryInStore();
expect(vm.createTempEntry).toHaveBeenCalledWith({
projectId: 'abcproject',
branchId: 'master',
parent: projectTree,
name: 'testing',
type,
});
});
it('sets editMode to true', (done) => {
vm.createEntryInStore();
setTimeout(() => {
expect(vm.$store.state.editMode).toBeTruthy();
done();
});
});
it('toggles blob view', (done) => {
vm.createEntryInStore();
setTimeout(() => {
expect(vm.$store.state.currentBlobView).toBe('repo-editor');
done();
});
});
it('opens newly created file', (done) => {
if (type === 'blob') {
vm.createEntryInStore();
setTimeout(() => {
expect(vm.$store.state.openFiles.length).toBe(1);
expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
done();
});
} else {
done();
}
});
if (type === 'blob') {
it('creates new file', (done) => {
vm.createEntryInStore();
setTimeout(() => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe('testing');
expect(baseTree[0].type).toBe('blob');
expect(baseTree[0].tempFile).toBeTruthy();
done();
});
});
it('does not create temp file when file already exists', (done) => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
baseTree.push(file('testing', '1', type));
vm.createEntryInStore();
setTimeout(() => {
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe('testing');
expect(baseTree[0].type).toBe('blob');
expect(baseTree[0].tempFile).toBeFalsy();
done();
});
});
} else {
it('creates new tree', () => {
vm.createEntryInStore();
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe('testing');
expect(baseTree[0].type).toBe('tree');
expect(baseTree[0].tempFile).toBeTruthy();
});
it('creates multiple trees when entryName has slashes', () => {
vm.entryName = 'app/test';
vm.createEntryInStore();
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe('app');
});
it('creates tree in existing tree', () => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app/test';
vm.createEntryInStore();
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe('app');
expect(baseTree[0].tempFile).toBeFalsy();
expect(baseTree[0].tree[0].tempFile).toBeTruthy();
expect(baseTree[0].tree[0].name).toBe('test');
});
it('does not create new tree when already exists', () => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app';
vm.createEntryInStore();
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe('app');
expect(baseTree[0].tempFile).toBeFalsy();
expect(baseTree[0].tree.length).toBe(0);
});
}
});
});
});
it('focuses field on mount', () => {
document.body.innerHTML += '<div class="js-test"></div>';
vm = createComponentWithStore(Component, store, {
type: 'tree',
projectId: 'abcproject',
branchId: 'master',
path: '',
}).$mount('.js-test');
expect(document.activeElement).toBe(vm.$refs.fieldName);
vm.$el.remove();
});
});
import Vue from 'vue';
import upload from '~/ide/components/new_dropdown/upload.vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('new dropdown upload', () => {
let vm;
let projectTree;
beforeEach(() => {
spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
data: {
id: '123',
},
}));
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
data: {
commit: {
id: '123branch',
},
},
}));
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
const Component = Vue.extend(upload);
store.state.projects.abcproject = {
web_url: '',
};
store.state.currentProjectId = 'abcproject';
store.state.trees = [];
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
vm = createComponentWithStore(Component, store, {
branchId: 'master',
path: '',
parent: projectTree,
});
vm.entryName = 'testing';
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('readFile', () => {
beforeEach(() => {
spyOn(FileReader.prototype, 'readAsText');
spyOn(FileReader.prototype, 'readAsDataURL');
});
it('calls readAsText for text files', () => {
const file = {
type: 'text/html',
};
vm.readFile(file);
expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
});
it('calls readAsDataURL for non-text files', () => {
const file = {
type: 'images/png',
};
vm.readFile(file);
expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
});
});
describe('createFile', () => {
const target = {
result: 'content',
};
const binaryTarget = {
result: 'base64,base64content',
};
const file = {
name: 'file',
};
it('creates new file', (done) => {
vm.createFile(target, file, true);
vm.$nextTick(() => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe(file.name);
expect(baseTree[0].content).toBe(target.result);
done();
});
});
it('creates new file in path', (done) => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
const tree = {
type: 'tree',
name: 'testing',
path: 'testing',
tree: [],
};
baseTree.push(tree);
vm.parent = tree;
vm.createFile(target, file, true);
vm.$nextTick(() => {
expect(baseTree.length).toBe(1);
expect(baseTree[0].tree[0].name).toBe(file.name);
expect(baseTree[0].tree[0].content).toBe(target.result);
expect(baseTree[0].tree[0].path).toBe(`testing/${file.name}`);
done();
});
});
it('splits content on base64 if binary', (done) => {
vm.createFile(binaryTarget, file, false);
vm.$nextTick(() => {
const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].name).toBe(file.name);
expect(baseTree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
expect(baseTree[0].base64).toBe(true);
done();
});
});
});
});
import Vue from 'vue';
import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/ide/stores';
import service from '~/ide/services';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
let vm;
function createComponent() {
const RepoCommitSection = Vue.extend(repoCommitSection);
const comp = new RepoCommitSection({
store,
}).$mount();
comp.$store.state.currentProjectId = 'abcproject';
comp.$store.state.currentBranchId = 'master';
comp.$store.state.projects.abcproject = {
web_url: '',
branches: {
master: {
workingReference: '1',
},
},
};
comp.$store.state.rightPanelCollapsed = false;
comp.$store.state.currentBranch = 'master';
comp.$store.state.openFiles = [file('file1'), file('file2')];
comp.$store.state.openFiles.forEach(f => Object.assign(f, {
changed: true,
content: 'testing',
}));
return comp.$mount();
}
beforeEach((done) => {
vm = createComponent();
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
Vue.nextTick(done);
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a commit section', () => {
const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
const submitCommit = vm.$el.querySelector('form .btn');
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path);
});
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
describe('when submitting', () => {
let changedFiles;
beforeEach(() => {
vm.commitMessage = 'testing';
changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles));
spyOn(service, 'commit').and.returnValue(Promise.resolve({
data: {
short_id: '1',
stats: {},
},
}));
});
it('allows you to submit', () => {
expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy();
});
it('submits commit', (done) => {
vm.makeCommit();
// Wait for the branch check to finish
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
const args = service.commit.calls.allArgs()[0];
const { commit_message, actions, branch: payloadBranch } = args[1];
expect(commit_message).toBe('testing');
expect(actions.length).toEqual(2);
expect(payloadBranch).toEqual('master');
expect(actions[0].action).toEqual('update');
expect(actions[1].action).toEqual('update');
expect(actions[0].content).toEqual(changedFiles[0].content);
expect(actions[1].content).toEqual(changedFiles[1].content);
expect(actions[0].file_path).toEqual(changedFiles[0].path);
expect(actions[1].file_path).toEqual(changedFiles[1].path);
})
.then(done)
.catch(done.fail);
});
it('redirects to MR creation page if start new MR checkbox checked', (done) => {
spyOn(urlUtils, 'visitUrl');
vm.startNewMR = true;
vm.makeCommit();
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
expect(urlUtils.visitUrl).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoEditButton from '~/ide/components/repo_edit_button.vue';
import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => {
let vm;
beforeEach(() => {
const f = file();
const RepoEditButton = Vue.extend(repoEditButton);
vm = new RepoEditButton({
store,
});
f.active = true;
vm.$store.dispatch('setInitialData', {
canCommit: true,
onTopOfBranch: true,
});
vm.$store.state.openFiles.push(f);
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders an edit button', () => {
vm.$mount();
expect(vm.$el.querySelector('.btn')).not.toBeNull();
expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
});
it('renders edit button with cancel text', () => {
vm.$store.state.editMode = true;
vm.$mount();
expect(vm.$el.querySelector('.btn')).not.toBeNull();
expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
});
it('toggles edit mode on click', (done) => {
vm.$mount();
vm.$el.querySelector('.btn').click();
vm.$nextTick(() => {
expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
done();
});
});
describe('discardPopupOpen', () => {
beforeEach(() => {
vm.$store.state.discardPopupOpen = true;
vm.$store.state.editMode = true;
vm.$store.state.openFiles[0].changed = true;
vm.$mount();
});
it('renders popup', () => {
expect(vm.$el.querySelector('.modal')).not.toBeNull();
});
it('removes all changed files', (done) => {
vm.$el.querySelector('.btn-warning').click();
vm.$nextTick(() => {
expect(vm.$store.getters.changedFiles.length).toBe(0);
expect(vm.$el.querySelector('.modal')).toBeNull();
done();
});
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
let vm;
beforeEach((done) => {
const f = file();
const RepoEditor = Vue.extend(repoEditor);
vm = new RepoEditor({
store,
});
f.active = true;
f.tempFile = true;
vm.$store.state.openFiles.push(f);
vm.$store.getters.activeFile.html = 'testing';
vm.monaco = true;
vm.$mount();
monacoLoader(['vs/editor/editor.main'], () => {
setTimeout(done, 0);
});
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders an ide container', (done) => {
Vue.nextTick(() => {
expect(vm.shouldHideEditor).toBeFalsy();
done();
});
});
describe('when open file is binary and not raw', () => {
beforeEach((done) => {
vm.$store.getters.activeFile.binary = true;
Vue.nextTick(done);
});
it('does not render the IDE', () => {
expect(vm.shouldHideEditor).toBeTruthy();
});
it('shows activeFile html', () => {
expect(vm.$el.textContent).toContain('testing');
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
import { file, resetStore } from '../helpers';
describe('RepoFileButtons', () => {
const activeFile = file();
let vm;
function createComponent() {
const RepoFileButtons = Vue.extend(repoFileButtons);
activeFile.rawPath = 'test';
activeFile.blamePath = 'test';
activeFile.commitsPath = 'test';
activeFile.active = true;
store.state.openFiles.push(activeFile);
return new RepoFileButtons({
store,
}).$mount();
}
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => {
vm = createComponent();
vm.$nextTick(() => {
const raw = vm.$el.querySelector('.raw');
const blame = vm.$el.querySelector('.blame');
const history = vm.$el.querySelector('.history');
expect(raw.href).toMatch(`/${activeFile.rawPath}`);
expect(raw.textContent.trim()).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blamePath}`);
expect(blame.textContent.trim()).toEqual('Blame');
expect(history.href).toMatch(`/${activeFile.commitsPath}`);
expect(history.textContent.trim()).toEqual('History');
expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
done();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoFile from '~/ide/components/repo_file.vue';
import { file, resetStore } from '../helpers';
describe('RepoFile', () => {
const updated = 'updated';
let vm;
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
return new RepoFile({
store,
propsData,
}).$mount();
}
afterEach(() => {
resetStore(vm.$store);
});
it('renders link, icon and name', () => {
const RepoFile = Vue.extend(repoFile);
vm = new RepoFile({
store,
propsData: {
file: file('t4'),
},
});
spyOn(vm, 'timeFormated').and.returnValue(updated);
vm.$mount();
const name = vm.$el.querySelector('.repo-file-name');
expect(name.href).toMatch('');
expect(name.textContent.trim()).toEqual(vm.file.name);
});
it('does render if hasFiles is true and is loading tree', () => {
vm = createComponent({
file: file('t1'),
});
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
});
it('does not render commit message and datetime if mini', (done) => {
vm = createComponent({
file: file('t2'),
});
vm.$store.state.openFiles.push(vm.file);
vm.$nextTick(() => {
expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
done();
});
});
it('fires clickFile when the link is clicked', () => {
vm = createComponent({
file: file('t3'),
});
spyOn(vm, 'clickFile');
vm.$el.click();
expect(vm.clickFile).toHaveBeenCalledWith(vm.file);
});
describe('submodule', () => {
let f;
beforeEach(() => {
f = file('submodule name', '123456789');
f.type = 'submodule';
vm = createComponent({
file: f,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders submodule short ID', () => {
expect(vm.$el.querySelector('.commit-sha').textContent.trim()).toBe('12345678');
});
it('renders ID next to submodule name', () => {
expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678');
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
import { resetStore } from '../helpers';
describe('RepoLoadingFile', () => {
let vm;
function createComponent() {
const RepoLoadingFile = Vue.extend(repoLoadingFile);
return new RepoLoadingFile({
store,
}).$mount();
}
function assertLines(lines) {
lines.forEach((line, n) => {
const index = n + 1;
expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
});
}
function assertColumns(columns) {
columns.forEach((column) => {
const container = column.querySelector('.animation-container');
const lines = [...container.querySelectorAll(':scope > div')];
expect(container).toBeTruthy();
expect(lines.length).toEqual(6);
assertLines(lines);
});
}
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders 3 columns of animated LoC', () => {
vm = createComponent();
const columns = [...vm.$el.querySelectorAll('td')];
expect(columns.length).toEqual(3);
assertColumns(columns);
});
it('renders 1 column of animated LoC if isMini', (done) => {
vm = createComponent();
vm.$store.state.leftPanelCollapsed = true;
vm.$store.state.openFiles.push('test');
vm.$nextTick(() => {
const columns = [...vm.$el.querySelectorAll('td')];
expect(columns.length).toEqual(1);
assertColumns(columns);
done();
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoPrevDirectory from '~/ide/components/repo_prev_directory.vue';
import { resetStore } from '../helpers';
describe('RepoPrevDirectory', () => {
let vm;
const parentLink = 'parent';
function createComponent() {
const RepoPrevDirectory = Vue.extend(repoPrevDirectory);
const comp = new RepoPrevDirectory({
store,
});
comp.$store.state.parentTreeUrl = parentLink;
return comp.$mount();
}
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a prev dir link', () => {
const link = vm.$el.querySelector('a');
expect(link.href).toMatch(`/${parentLink}`);
expect(link.textContent).toEqual('...');
});
it('clicking row triggers getTreeData', () => {
spyOn(vm, 'getTreeData');
vm.$el.querySelector('td').click();
expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink });
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoPreview from '~/ide/components/repo_preview.vue';
import { file, resetStore } from '../helpers';
describe('RepoPreview', () => {
let vm;
function createComponent() {
const f = file();
const RepoPreview = Vue.extend(repoPreview);
const comp = new RepoPreview({
store,
});
f.active = true;
f.html = 'test';
comp.$store.state.openFiles.push(f);
return comp.$mount();
}
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a div with the activeFile html', () => {
vm = createComponent();
expect(vm.$el.tagName).toEqual('DIV');
expect(vm.$el.innerHTML).toContain('test');
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoTab from '~/ide/components/repo_tab.vue';
import { file, resetStore } from '../helpers';
describe('RepoTab', () => {
let vm;
function createComponent(propsData) {
const RepoTab = Vue.extend(repoTab);
return new RepoTab({
store,
propsData,
}).$mount();
}
afterEach(() => {
resetStore(vm.$store);
});
it('renders a close link and a name link', () => {
vm = createComponent({
tab: file(),
});
vm.$store.state.openFiles.push(vm.tab);
const close = vm.$el.querySelector('.multi-file-tab-close');
const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
expect(close.querySelector('.fa-times')).toBeTruthy();
expect(name.textContent.trim()).toEqual(vm.tab.name);
});
it('fires clickFile when the link is clicked', () => {
vm = createComponent({
tab: file(),
});
spyOn(vm, 'clickFile');
vm.$el.click();
expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
});
it('calls closeFile when clicking close button', () => {
vm = createComponent({
tab: file(),
});
spyOn(vm, 'closeFile');
vm.$el.querySelector('.multi-file-tab-close').click();
expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab });
});
it('renders an fa-circle icon if tab is changed', () => {
const tab = file('changedFile');
tab.changed = true;
vm = createComponent({
tab,
});
expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull();
});
describe('methods', () => {
describe('closeTab', () => {
it('does not close tab if is changed', (done) => {
const tab = file('closeFile');
tab.changed = true;
tab.opened = true;
vm = createComponent({
tab,
});
vm.$store.state.openFiles.push(tab);
vm.$store.dispatch('setFileActive', tab);
vm.$el.querySelector('.multi-file-tab-close').click();
vm.$nextTick(() => {
expect(tab.opened).toBeTruthy();
done();
});
});
it('closes tab when clicking close btn', (done) => {
const tab = file('lose');
tab.opened = true;
vm = createComponent({
tab,
});
vm.$store.state.openFiles.push(tab);
vm.$store.dispatch('setFileActive', tab);
vm.$el.querySelector('.multi-file-tab-close').click();
vm.$nextTick(() => {
expect(tab.opened).toBeFalsy();
done();
});
});
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoTabs from '~/ide/components/repo_tabs.vue';
import { file, resetStore } from '../helpers';
describe('RepoTabs', () => {
const openedFiles = [file('open1'), file('open2')];
let vm;
function createComponent() {
const RepoTabs = Vue.extend(repoTabs);
return new RepoTabs({
store,
}).$mount();
}
afterEach(() => {
resetStore(vm.$store);
});
it('renders a list of tabs', (done) => {
vm = createComponent();
openedFiles[0].active = true;
vm.$store.state.openFiles = openedFiles;
vm.$nextTick(() => {
const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
expect(tabs.length).toEqual(2);
expect(tabs[0].classList.contains('active')).toBeTruthy();
expect(tabs[1].classList.contains('active')).toBeFalsy();
done();
});
});
});
import { decorateData } from '~/ide/stores/utils';
import state from '~/ide/stores/state';
export const resetStore = (store) => {
store.replaceState(state());
};
export const file = (name = 'name', id = name, type = '') => decorateData({
id,
type,
icon: 'icon',
url: 'url',
name,
path: name,
lastCommit: {},
});
import Disposable from '~/ide/lib/common/disposable';
describe('Multi-file editor library disposable class', () => {
let instance;
let disposableClass;
beforeEach(() => {
instance = new Disposable();
disposableClass = {
dispose: jasmine.createSpy('dispose'),
};
});
afterEach(() => {
instance.dispose();
});
describe('add', () => {
it('adds disposable classes', () => {
instance.add(disposableClass);
expect(instance.disposers.size).toBe(1);
});
});
describe('dispose', () => {
beforeEach(() => {
instance.add(disposableClass);
});
it('calls dispose on all cached disposers', () => {
instance.dispose();
expect(disposableClass.dispose).toHaveBeenCalled();
});
it('clears cached disposers', () => {
instance.dispose();
expect(instance.disposers.size).toBe(0);
});
});
});
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import ModelManager from '~/ide/lib/common/model_manager';
import { file } from '../../helpers';
describe('Multi-file editor library model manager', () => {
let instance;
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => {
instance = new ModelManager(monaco);
done();
});
});
afterEach(() => {
instance.dispose();
});
describe('addModel', () => {
it('caches model', () => {
instance.addModel(file());
expect(instance.models.size).toBe(1);
});
it('caches model by file path', () => {
instance.addModel(file('path-name'));
expect(instance.models.keys().next().value).toBe('path-name');
});
it('adds model into disposable', () => {
spyOn(instance.disposable, 'add').and.callThrough();
instance.addModel(file());
expect(instance.disposable.add).toHaveBeenCalled();
});
it('returns cached model', () => {
spyOn(instance.models, 'get').and.callThrough();
instance.addModel(file());
instance.addModel(file());
expect(instance.models.get).toHaveBeenCalled();
});
});
describe('hasCachedModel', () => {
it('returns false when no models exist', () => {
expect(instance.hasCachedModel('path')).toBeFalsy();
});
it('returns true when model exists', () => {
instance.addModel(file('path-name'));
expect(instance.hasCachedModel('path-name')).toBeTruthy();
});
});
describe('dispose', () => {
it('clears cached models', () => {
instance.addModel(file());
instance.dispose();
expect(instance.models.size).toBe(0);
});
it('calls disposable dispose', () => {
spyOn(instance.disposable, 'dispose').and.callThrough();
instance.dispose();
expect(instance.disposable.dispose).toHaveBeenCalled();
});
});
});
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library model', () => {
let model;
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path'));
done();
});
});
afterEach(() => {
model.dispose();
});
it('creates original model & new model', () => {
expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull();
});
describe('path', () => {
it('returns file path', () => {
expect(model.path).toBe('path');
});
});
describe('getModel', () => {
it('returns model', () => {
expect(model.getModel()).toBe(model.model);
});
});
describe('getOriginalModel', () => {
it('returns original model', () => {
expect(model.getOriginalModel()).toBe(model.originalModel);
});
});
describe('onChange', () => {
it('caches event by path', () => {
model.onChange(() => {});
expect(model.events.size).toBe(1);
expect(model.events.keys().next().value).toBe('path');
});
it('calls callback on change', (done) => {
const spy = jasmine.createSpy();
model.onChange(spy);
model.getModel().setValue('123');
setTimeout(() => {
expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything());
done();
});
});
});
describe('dispose', () => {
it('calls disposable dispose', () => {
spyOn(model.disposable, 'dispose').and.callThrough();
model.dispose();
expect(model.disposable.dispose).toHaveBeenCalled();
});
it('clears events', () => {
model.onChange(() => {});
expect(model.events.size).toBe(1);
model.dispose();
expect(model.events.size).toBe(0);
});
});
});
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import DecorationsController from '~/ide/lib/decorations/controller';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library decorations controller', () => {
let editorInstance;
let controller;
let model;
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => {
editorInstance = editor.create(monaco);
editorInstance.createInstance(document.createElement('div'));
controller = new DecorationsController(editorInstance);
model = new Model(monaco, file('path'));
done();
});
});
afterEach(() => {
model.dispose();
editorInstance.dispose();
controller.dispose();
});
describe('getAllDecorationsForModel', () => {
it('returns empty array when no decorations exist for model', () => {
const decorations = controller.getAllDecorationsForModel(model);
expect(decorations).toEqual([]);
});
it('returns decorations by model URL', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
const decorations = controller.getAllDecorationsForModel(model);
expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
});
});
describe('addDecorations', () => {
it('caches decorations in a new map', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorations.size).toBe(1);
});
it('does not create new cache model', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
expect(controller.decorations.size).toBe(1);
});
it('caches decorations by model URL', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorations.size).toBe(1);
expect(controller.decorations.keys().next().value).toBe('path');
});
it('calls decorate method', () => {
spyOn(controller, 'decorate');
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorate).toHaveBeenCalled();
});
});
describe('decorate', () => {
it('sets decorations on editor instance', () => {
spyOn(controller.editor.instance, 'deltaDecorations');
controller.decorate(model);
expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
});
it('caches decorations', () => {
spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
controller.decorate(model);
expect(controller.editorDecorations.size).toBe(1);
});
it('caches decorations by model URL', () => {
spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
controller.decorate(model);
expect(controller.editorDecorations.keys().next().value).toBe('path');
});
});
describe('dispose', () => {
it('clears cached decorations', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
controller.dispose();
expect(controller.decorations.size).toBe(0);
});
it('clears cached editorDecorations', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
controller.dispose();
expect(controller.editorDecorations.size).toBe(0);
});
});
});
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
describe('Multi-file editor library dirty diff controller', () => {
let editorInstance;
let controller;
let modelManager;
let decorationsController;
let model;
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => {
editorInstance = editor.create(monaco);
editorInstance.createInstance(document.createElement('div'));
modelManager = new ModelManager(monaco);
decorationsController = new DecorationsController(editorInstance);
model = modelManager.addModel(file());
controller = new DirtyDiffController(modelManager, decorationsController);
done();
});
});
afterEach(() => {
controller.dispose();
model.dispose();
decorationsController.dispose();
editorInstance.dispose();
});
describe('getDiffChangeType', () => {
['added', 'removed', 'modified'].forEach((type) => {
it(`returns ${type}`, () => {
const change = {
[type]: true,
};
expect(getDiffChangeType(change)).toBe(type);
});
});
});
describe('getDecorator', () => {
['added', 'removed', 'modified'].forEach((type) => {
it(`returns with linesDecorationsClassName for ${type}`, () => {
const change = {
[type]: true,
};
expect(
getDecorator(change).options.linesDecorationsClassName,
).toBe(`dirty-diff dirty-diff-${type}`);
});
it('returns with line numbers', () => {
const change = {
lineNumber: 1,
endLineNumber: 2,
[type]: true,
};
const range = getDecorator(change).range;
expect(range.startLineNumber).toBe(1);
expect(range.endLineNumber).toBe(2);
expect(range.startColumn).toBe(1);
expect(range.endColumn).toBe(1);
});
});
});
describe('attachModel', () => {
it('adds change event callback', () => {
spyOn(model, 'onChange');
controller.attachModel(model);
expect(model.onChange).toHaveBeenCalled();
});
it('calls throttledComputeDiff on change', () => {
spyOn(controller, 'throttledComputeDiff');
controller.attachModel(model);
model.getModel().setValue('123');
expect(controller.throttledComputeDiff).toHaveBeenCalled();
});
});
describe('computeDiff', () => {
it('posts to worker', () => {
spyOn(controller.dirtyDiffWorker, 'postMessage');
controller.computeDiff(model);
expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
path: model.path,
originalContent: '',
newContent: '',
});
});
});
describe('reDecorate', () => {
it('calls decorations controller decorate', () => {
spyOn(controller.decorationsController, 'decorate');
controller.reDecorate(model);
expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
});
});
describe('decorate', () => {
it('adds decorations into decorations controller', () => {
spyOn(controller.decorationsController, 'addDecorations');
controller.decorate({ data: { changes: [], path: 'path' } });
expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything());
});
it('adds decorations into editor', () => {
const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } });
expect(spy).toHaveBeenCalledWith([], [{
range: new monaco.Range(
1, 1, 1, 1,
),
options: {
isWholeLine: true,
linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
},
}]);
});
});
describe('dispose', () => {
it('calls disposable dispose', () => {
spyOn(controller.disposable, 'dispose').and.callThrough();
controller.dispose();
expect(controller.disposable.dispose).toHaveBeenCalled();
});
it('terminates worker', () => {
spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
controller.dispose();
expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
});
it('removes worker event listener', () => {
spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
controller.dispose();
expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything());
});
});
});
import { computeDiff } from '~/ide/lib/diff/diff';
describe('Multi-file editor library diff calculator', () => {
describe('computeDiff', () => {
it('returns empty array if no changes', () => {
const diff = computeDiff('123', '123');
expect(diff).toEqual([]);
});
describe('modified', () => {
it('', () => {
const diff = computeDiff('123', '1234')[0];
expect(diff.added).toBeTruthy();
expect(diff.modified).toBeTruthy();
expect(diff.removed).toBeUndefined();
});
it('', () => {
const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
expect(diff.added).toBeTruthy();
expect(diff.modified).toBeTruthy();
expect(diff.removed).toBeUndefined();
expect(diff.lineNumber).toBe(2);
});
});
describe('added', () => {
it('', () => {
const diff = computeDiff('123', '123\n123')[0];
expect(diff.added).toBeTruthy();
expect(diff.modified).toBeUndefined();
expect(diff.removed).toBeUndefined();
});
it('', () => {
const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
expect(diff.added).toBeTruthy();
expect(diff.modified).toBeUndefined();
expect(diff.removed).toBeUndefined();
expect(diff.lineNumber).toBe(3);
});
});
describe('removed', () => {
it('', () => {
const diff = computeDiff('123', '')[0];
expect(diff.added).toBeUndefined();
expect(diff.modified).toBeUndefined();
expect(diff.removed).toBeTruthy();
});
it('', () => {
const diff = computeDiff('123\n123\n123', '123\n123')[0];
expect(diff.added).toBeUndefined();
expect(diff.modified).toBeTruthy();
expect(diff.removed).toBeTruthy();
expect(diff.lineNumber).toBe(2);
});
});
it('includes line number of change', () => {
const diff = computeDiff('123', '')[0];
expect(diff.lineNumber).toBe(1);
});
it('includes end line number of change', () => {
const diff = computeDiff('123', '')[0];
expect(diff.endLineNumber).toBe(1);
});
});
});
import editorOptions from '~/ide/lib/editor_options';
describe('Multi-file editor library editor options', () => {
it('returns an array', () => {
expect(editorOptions).toEqual(jasmine.any(Array));
});
});
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
let instance;
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => {
instance = editor.create(monaco);
done();
});
});
afterEach(() => {
instance.dispose();
});
it('creates instance of editor', () => {
expect(editor.editorInstance).not.toBeNull();
});
describe('createInstance', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough();
instance.createInstance(el);
expect(instance.monaco.editor.create).toHaveBeenCalled();
});
it('creates dirty diff controller', () => {
instance.createInstance(el);
expect(instance.dirtyDiffController).not.toBeNull();
});
});
describe('createModel', () => {
it('calls model manager addModel', () => {
spyOn(instance.modelManager, 'addModel');
instance.createModel('FILE');
expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
});
});
describe('attachModel', () => {
let model;
beforeEach(() => {
instance.createInstance(document.createElement('div'));
model = instance.createModel(file());
});
it('sets the current model on the instance', () => {
instance.attachModel(model);
expect(instance.currentModel).toBe(model);
});
it('attaches the model to the current instance', () => {
spyOn(instance.instance, 'setModel');
instance.attachModel(model);
expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
});
it('attaches the model to the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'attachModel');
instance.attachModel(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
});
it('re-decorates with the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'reDecorate');
instance.attachModel(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
});
});
describe('clearEditor', () => {
it('resets the editor model', () => {
instance.createInstance(document.createElement('div'));
spyOn(instance.instance, 'setModel');
instance.clearEditor();
expect(instance.instance.setModel).toHaveBeenCalledWith(null);
});
});
describe('dispose', () => {
it('calls disposble dispose method', () => {
spyOn(instance.disposable, 'dispose').and.callThrough();
instance.dispose();
expect(instance.disposable.dispose).toHaveBeenCalled();
});
it('resets instance', () => {
instance.createInstance(document.createElement('div'));
expect(instance.instance).not.toBeNull();
instance.dispose();
expect(instance.instance).toBeNull();
});
});
});
import monacoContext from 'monaco-editor/dev/vs/loader';
import monacoLoader from '~/ide/monaco_loader';
describe('MonacoLoader', () => {
it('calls require.config and exports require', () => {
expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
},
}));
expect(monacoLoader).toBe(monacoContext.require);
});
});
import store from '~/ide/stores';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('Multi-file store branch actions', () => {
afterEach(() => {
resetStore(store);
});
describe('createNewBranch', () => {
beforeEach(() => {
spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
json: () => ({
name: 'testing',
}),
}));
spyOn(history, 'pushState');
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'testing';
store.state.projects.abcproject = {
branches: {
master: {
workingReference: '1',
},
},
};
});
it('creates new branch', (done) => {
store.dispatch('createNewBranch', 'master')
.then(() => {
expect(store.state.currentBranchId).toBe('testing');
expect(service.createBranch).toHaveBeenCalledWith('abcproject', {
branch: 'master',
ref: 'testing',
});
done();
})
.catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
afterEach(() => {
resetStore(store);
});
describe('closeFile', () => {
let localFile;
let getLastCommitDataSpy;
let oldGetLastCommitData;
beforeEach(() => {
getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
localFile = file('testFile');
localFile.active = true;
localFile.opened = true;
localFile.parentTreeUrl = 'parentTreeUrl';
store.state.openFiles.push(localFile);
});
afterEach(() => {
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
});
it('closes open files', (done) => {
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy();
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('does not close file if has changed', (done) => {
localFile.changed = true;
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(localFile.opened).toBeTruthy();
expect(localFile.active).toBeTruthy();
expect(store.state.openFiles.length).toBe(1);
done();
}).catch(done.fail);
});
it('does not close file if temp file', (done) => {
localFile.tempFile = true;
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(localFile.opened).toBeTruthy();
expect(localFile.active).toBeTruthy();
expect(store.state.openFiles.length).toBe(1);
done();
}).catch(done.fail);
});
it('force closes a changed file', (done) => {
localFile.changed = true;
store.dispatch('closeFile', { file: localFile, force: true })
.then(() => {
expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy();
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('sets next file as active', (done) => {
const f = file('otherfile');
store.state.openFiles.push(f);
expect(f.active).toBeFalsy();
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(f.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('calls getLastCommitData', (done) => {
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(getLastCommitDataSpy).toHaveBeenCalled();
done();
}).catch(done.fail);
});
});
describe('setFileActive', () => {
let scrollToTabSpy;
let oldScrollToTab;
beforeEach(() => {
scrollToTabSpy = jasmine.createSpy('scrollToTab');
oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
});
afterEach(() => {
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
});
it('calls scrollToTab', (done) => {
store.dispatch('setFileActive', file('setThisActive'))
.then(() => {
expect(scrollToTabSpy).toHaveBeenCalled();
done();
}).catch(done.fail);
});
it('sets the file active', (done) => {
const localFile = file('activeFile');
store.dispatch('setFileActive', localFile)
.then(() => {
expect(localFile.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('returns early if file is already active', (done) => {
const localFile = file('earlyActive');
localFile.active = true;
store.dispatch('setFileActive', localFile)
.then(() => {
expect(scrollToTabSpy).not.toHaveBeenCalled();
done();
}).catch(done.fail);
});
it('sets current active file to not active', (done) => {
const localFile = file('currentActive');
localFile.active = true;
store.state.openFiles.push(localFile);
store.dispatch('setFileActive', file('newActive'))
.then(() => {
expect(localFile.active).toBeFalsy();
done();
}).catch(done.fail);
});
it('resets location.hash for line highlighting', (done) => {
location.hash = 'test';
store.dispatch('setFileActive', file('otherActive'))
.then(() => {
expect(location.hash).not.toBe('test');
done();
}).catch(done.fail);
});
});
describe('getFileData', () => {
let localFile;
beforeEach(() => {
spyOn(service, 'getFileData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'testing getFileData',
},
json: () => Promise.resolve({
blame_path: 'blame_path',
commits_path: 'commits_path',
permalink: 'permalink',
raw_path: 'raw_path',
binary: false,
html: '123',
render_error: '',
}),
}));
localFile = file('newCreate');
localFile.url = 'getFileDataURL';
});
afterEach(() => {
store.dispatch('closeFile', {
file: localFile,
force: true,
});
});
it('calls the service', (done) => {
store.dispatch('getFileData', localFile)
.then(() => {
expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
done();
}).catch(done.fail);
});
it('sets the file data', (done) => {
store.dispatch('getFileData', localFile)
.then(Vue.nextTick)
.then(() => {
expect(localFile.blamePath).toBe('blame_path');
done();
}).catch(done.fail);
});
it('sets document title', (done) => {
store.dispatch('getFileData', localFile)
.then(() => {
expect(document.title).toBe('testing getFileData');
done();
}).catch(done.fail);
});
it('sets the file as active', (done) => {
store.dispatch('getFileData', localFile)
.then(Vue.nextTick)
.then(() => {
expect(localFile.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('adds the file to open files', (done) => {
store.dispatch('getFileData', localFile)
.then(Vue.nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(localFile.name);
done();
}).catch(done.fail);
});
it('toggles the file loading', (done) => {
store.dispatch('getFileData', localFile)
.then(() => {
expect(localFile.loading).toBeTruthy();
return Vue.nextTick();
})
.then(() => {
expect(localFile.loading).toBeFalsy();
done();
}).catch(done.fail);
});
});
describe('getRawFileData', () => {
let tmpFile;
beforeEach(() => {
spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
tmpFile = file('tmpFile');
});
it('calls getRawFileData service method', (done) => {
store.dispatch('getRawFileData', tmpFile)
.then(() => {
expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
done();
}).catch(done.fail);
});
it('updates file raw data', (done) => {
store.dispatch('getRawFileData', tmpFile)
.then(() => {
expect(tmpFile.raw).toBe('raw');
done();
}).catch(done.fail);
});
});
describe('changeFileContent', () => {
let tmpFile;
beforeEach(() => {
tmpFile = file('tmpFile');
});
it('updates file content', (done) => {
store.dispatch('changeFileContent', {
file: tmpFile,
content: 'content',
})
.then(() => {
expect(tmpFile.content).toBe('content');
done();
}).catch(done.fail);
});
});
describe('createTempFile', () => {
let projectTree;
beforeEach(() => {
document.body.innerHTML += '<div class="flash-container"></div>';
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
branches: {
master: {
workingReference: '1',
},
},
};
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
});
afterEach(() => {
document.querySelector('.flash-container').remove();
});
it('creates temp file', (done) => {
store.dispatch('createTempFile', {
name: 'test',
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
}).then((f) => {
expect(f.tempFile).toBeTruthy();
expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
done();
}).catch(done.fail);
});
it('adds tmp file to open files', (done) => {
store.dispatch('createTempFile', {
name: 'test',
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
}).then((f) => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(f.name);
done();
}).catch(done.fail);
});
it('sets tmp file as active', (done) => {
store.dispatch('createTempFile', {
name: 'test',
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
}).then((f) => {
expect(f.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('enters edit mode if file is not base64', (done) => {
store.dispatch('createTempFile', {
name: 'test',
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
}).then(() => {
expect(store.state.editMode).toBeTruthy();
done();
}).catch(done.fail);
});
it('creates flash message is file already exists', (done) => {
store.state.trees['abcproject/mybranch'].tree.push(file('test', '1', 'blob'));
store.dispatch('createTempFile', {
name: 'test',
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
}).then(() => {
expect(document.querySelector('.flash-alert')).not.toBeNull();
done();
}).catch(done.fail);
});
it('increases level of file', (done) => {
store.state.trees['abcproject/mybranch'].level = 1;
store.dispatch('createTempFile', {
name: 'test',
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
}).then((f) => {
expect(f.level).toBe(2);
done();
}).catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store tree actions', () => {
let projectTree;
const basicCallParameters = {
endpoint: 'rootEndpoint',
projectId: 'abcproject',
branch: 'master',
};
beforeEach(() => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
web_url: '',
branches: {
master: {
workingReference: '1',
},
},
};
});
afterEach(() => {
resetStore(store);
});
describe('getTreeData', () => {
beforeEach(() => {
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
});
it('calls service getTreeData', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
done();
}).catch(done.fail);
});
it('adds data into tree', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
projectTree = store.state.trees['abcproject/master'];
expect(projectTree.tree.length).toBe(3);
expect(projectTree.tree[0].type).toBe('tree');
expect(projectTree.tree[1].type).toBe('submodule');
expect(projectTree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
it('sets parent tree URL', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.parentTreeUrl).toBe('parent_tree_url');
done();
}).catch(done.fail);
});
it('sets last commit path', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path');
done();
}).catch(done.fail);
});
it('sets root if not currently at root', (done) => {
store.state.isInitialRoot = false;
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.isInitialRoot).toBeTruthy();
expect(store.state.isRoot).toBeTruthy();
done();
}).catch(done.fail);
});
it('sets page title', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(document.title).toBe('test');
done();
}).catch(done.fail);
});
it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
store.state.prevLastCommitPath = 'test';
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree);
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
done();
}).catch(done.fail);
});
});
describe('toggleTreeOpen', () => {
let oldGetTreeData;
let getTreeDataSpy;
let tree;
beforeEach(() => {
getTreeDataSpy = jasmine.createSpy('getTreeData');
oldGetTreeData = store._actions.getTreeData; // eslint-disable-line
store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
tree = {
projectId: 'abcproject',
branchId: 'master',
opened: false,
tree: [],
};
});
afterEach(() => {
store._actions.getTreeData = oldGetTreeData; // eslint-disable-line
});
it('toggles the tree open', (done) => {
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(tree.opened).toBeTruthy();
done();
}).catch(done.fail);
});
it('calls getTreeData if tree is closed', (done) => {
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(getTreeDataSpy).toHaveBeenCalledWith({
projectId: 'abcproject',
branch: 'master',
endpoint: 'test',
tree,
});
done();
}).catch(done.fail);
});
it('resets entries tree', (done) => {
Object.assign(tree, {
opened: true,
tree: ['a'],
});
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(tree.tree.length).toBe(0);
done();
}).catch(done.fail);
});
});
describe('createTempTree', () => {
beforeEach(() => {
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
});
it('creates temp tree', (done) => {
store.dispatch('createTempTree', {
projectId: store.state.currentProjectId,
branchId: store.state.currentBranchId,
name: 'test',
parent: projectTree,
})
.then(() => {
expect(projectTree.tree[0].name).toBe('test');
expect(projectTree.tree[0].type).toBe('tree');
done();
}).catch(done.fail);
});
it('creates new folder inside another tree', (done) => {
const tree = {
type: 'tree',
name: 'testing',
tree: [],
};
projectTree.tree.push(tree);
store.dispatch('createTempTree', {
projectId: store.state.currentProjectId,
branchId: store.state.currentBranchId,
name: 'testing/test',
parent: projectTree,
})
.then(() => {
expect(projectTree.tree[0].name).toBe('testing');
expect(projectTree.tree[0].tree[0].tempFile).toBeTruthy();
expect(projectTree.tree[0].tree[0].name).toBe('test');
expect(projectTree.tree[0].tree[0].type).toBe('tree');
done();
}).catch(done.fail);
});
it('does not create new tree if already exists', (done) => {
const tree = {
type: 'tree',
name: 'testing',
endpoint: 'test',
tree: [],
};
projectTree.tree.push(tree);
store.dispatch('createTempTree', {
projectId: store.state.currentProjectId,
branchId: store.state.currentBranchId,
name: 'testing/test',
parent: projectTree,
})
.then(() => {
expect(projectTree.tree[0].name).toBe('testing');
expect(projectTree.tree[0].tempFile).toBeUndefined();
done();
}).catch(done.fail);
});
});
describe('getLastCommitData', () => {
beforeEach(() => {
spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({
headers: {
'more-logs-url': null,
},
json: () => Promise.resolve([{
type: 'tree',
file_name: 'testing',
commit: {
message: 'commit message',
authored_date: '123',
},
}]),
}));
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
projectTree = store.state.trees['abcproject/mybranch'];
projectTree.tree.push(file('testing', '1', 'tree'));
projectTree.lastCommitPath = 'lastcommitpath';
});
it('calls service with lastCommitPath', (done) => {
store.dispatch('getLastCommitData', projectTree)
.then(() => {
expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
done();
}).catch(done.fail);
});
it('updates trees last commit data', (done) => {
store.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
done();
}).catch(done.fail);
});
it('does not update entry if not found', (done) => {
projectTree.tree[0].name = 'a';
store.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
done();
}).catch(done.fail);
});
});
describe('updateDirectoryData', () => {
it('adds data into tree', (done) => {
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [{ name: 'blob' }],
};
store.dispatch('updateDirectoryData', {
data,
tree,
}).then(() => {
expect(tree.tree[0].name).toBe('tree');
expect(tree.tree[0].type).toBe('tree');
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[1].type).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
expect(tree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
});
});
import Vue from 'vue';
import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/ide/stores';
import service from '~/ide/services';
import { resetStore, file } from '../helpers';
describe('Multi-file store actions', () => {
afterEach(() => {
resetStore(store);
});
describe('redirectToUrl', () => {
it('calls visitUrl', (done) => {
spyOn(urlUtils, 'visitUrl');
store.dispatch('redirectToUrl', 'test')
.then(() => {
expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
done();
})
.catch(done.fail);
});
});
describe('setInitialData', () => {
it('commits initial data', (done) => {
store.dispatch('setInitialData', { canCommit: true })
.then(() => {
expect(store.state.canCommit).toBeTruthy();
done();
})
.catch(done.fail);
});
});
describe('closeDiscardPopup', () => {
it('closes the discard popup', (done) => {
store.dispatch('closeDiscardPopup', false)
.then(() => {
expect(store.state.discardPopupOpen).toBeFalsy();
done();
})
.catch(done.fail);
});
});
describe('discardAllChanges', () => {
beforeEach(() => {
store.state.openFiles.push(file('discardAll'));
store.state.openFiles[0].changed = true;
});
});
describe('closeAllFiles', () => {
beforeEach(() => {
store.state.openFiles.push(file('closeAll'));
store.state.openFiles[0].opened = true;
});
it('closes all open files', (done) => {
store.dispatch('closeAllFiles')
.then(() => {
expect(store.state.openFiles.length).toBe(0);
done();
})
.catch(done.fail);
});
});
describe('toggleEditMode', () => {
it('toggles edit mode', (done) => {
store.state.editMode = true;
store.dispatch('toggleEditMode')
.then(() => {
expect(store.state.editMode).toBeFalsy();
done();
}).catch(done.fail);
});
it('sets preview mode', (done) => {
store.state.currentBlobView = 'repo-editor';
store.state.editMode = true;
store.dispatch('toggleEditMode')
.then(Vue.nextTick)
.then(() => {
expect(store.state.currentBlobView).toBe('repo-preview');
done();
}).catch(done.fail);
});
it('opens discard popup if there are changed files', (done) => {
store.state.editMode = true;
store.state.openFiles.push(file('discardChanges'));
store.state.openFiles[0].changed = true;
store.dispatch('toggleEditMode')
.then(() => {
expect(store.state.discardPopupOpen).toBeTruthy();
done();
}).catch(done.fail);
});
it('can force closed if there are changed files', (done) => {
store.state.editMode = true;
store.state.openFiles.push(file('forceClose'));
store.state.openFiles[0].changed = true;
store.dispatch('toggleEditMode', true)
.then(() => {
expect(store.state.discardPopupOpen).toBeFalsy();
expect(store.state.editMode).toBeFalsy();
done();
}).catch(done.fail);
});
it('discards file changes', (done) => {
const f = file('discard');
store.state.editMode = true;
store.state.openFiles.push(f);
f.changed = true;
store.dispatch('toggleEditMode', true)
.then(Vue.nextTick)
.then(() => {
expect(f.changed).toBeFalsy();
done();
}).catch(done.fail);
});
});
describe('toggleBlobView', () => {
it('sets edit mode view if in edit mode', (done) => {
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-editor');
done();
})
.catch(done.fail);
});
it('sets preview mode view if not in edit mode', (done) => {
store.state.editMode = false;
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-preview');
done();
})
.catch(done.fail);
});
});
describe('checkCommitStatus', () => {
beforeEach(() => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
branches: {
master: {
workingReference: '1',
},
},
};
});
it('calls service', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
data: {
commit: { id: '123' },
},
}));
store.dispatch('checkCommitStatus')
.then(() => {
expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
done();
})
.catch(done.fail);
});
it('returns true if current ref does not equal returned ID', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
data: {
commit: { id: '123' },
},
}));
store.dispatch('checkCommitStatus')
.then((val) => {
expect(val).toBeTruthy();
done();
})
.catch(done.fail);
});
it('returns false if current ref equals returned ID', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
data: {
commit: { id: '1' },
},
}));
store.dispatch('checkCommitStatus')
.then((val) => {
expect(val).toBeFalsy();
done();
})
.catch(done.fail);
});
});
describe('commitChanges', () => {
let payload;
beforeEach(() => {
spyOn(window, 'scrollTo');
document.body.innerHTML += '<div class="flash-container"></div>';
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
web_url: 'webUrl',
branches: {
master: {
workingReference: '1',
},
},
};
payload = {
branch: 'master',
};
});
afterEach(() => {
document.querySelector('.flash-container').remove();
});
describe('success', () => {
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(Promise.resolve({
data: {
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
stats: {
additions: '1',
deletions: '2',
},
},
}));
});
it('calls service', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(service.commit).toHaveBeenCalledWith('abcproject', payload);
done();
}).catch(done.fail);
});
it('shows flash notice', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
const alert = document.querySelector('.flash-container');
expect(alert.querySelector('.flash-notice')).not.toBeNull();
expect(alert.textContent.trim()).toBe(
'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.',
);
done();
}).catch(done.fail);
});
it('adds commit data to changed files', (done) => {
const changedFile = file('changed');
const f = file('newfile');
changedFile.changed = true;
store.state.openFiles.push(changedFile, f);
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(changedFile.lastCommit.message).toBe('test message');
expect(f.lastCommit.message).not.toBe('test message');
done();
}).catch(done.fail);
});
it('scrolls to top of page', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
done();
}).catch(done.fail);
});
it('redirects to new merge request page', (done) => {
spyOn(urlUtils, 'visitUrl');
store.dispatch('commitChanges', { payload, newMr: true })
.then(() => {
expect(urlUtils.visitUrl).toHaveBeenCalledWith('webUrl/merge_requests/new?merge_request%5Bsource_branch%5D=master');
done();
}).catch(done.fail);
});
});
describe('failed', () => {
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(Promise.resolve({
data: {
message: 'failed message',
},
}));
});
it('shows failed message', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
const alert = document.querySelector('.flash-container');
expect(alert.textContent.trim()).toBe(
'failed message',
);
done();
}).catch(done.fail);
});
});
});
describe('createTempEntry', () => {
beforeEach(() => {
store.state.trees['abcproject/mybranch'] = {
tree: [],
};
store.state.projects.abcproject = {
web_url: '',
};
});
it('creates a temp tree', (done) => {
const projectTree = store.state.trees['abcproject/mybranch'];
store.dispatch('createTempEntry', {
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
name: 'test',
type: 'tree',
})
.then(() => {
const baseTree = projectTree.tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].tempFile).toBeTruthy();
expect(baseTree[0].type).toBe('tree');
done();
})
.catch(done.fail);
});
it('creates temp file', (done) => {
const projectTree = store.state.trees['abcproject/mybranch'];
store.dispatch('createTempEntry', {
projectId: 'abcproject',
branchId: 'mybranch',
parent: projectTree,
name: 'test',
type: 'blob',
})
.then(() => {
const baseTree = projectTree.tree;
expect(baseTree.length).toBe(1);
expect(baseTree[0].tempFile).toBeTruthy();
expect(baseTree[0].type).toBe('blob');
done();
})
.catch(done.fail);
});
});
describe('popHistoryState', () => {
});
describe('scrollToTab', () => {
it('focuses the current active element', (done) => {
document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
const el = document.querySelector('.repo-tab');
spyOn(el, 'focus');
store.dispatch('scrollToTab')
.then(() => {
setTimeout(() => {
expect(el.focus).toHaveBeenCalled();
document.getElementById('tabs').remove();
done();
});
})
.catch(done.fail);
});
});
});
import * as getters from '~/ide/stores/getters';
import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store getters', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('changedFiles', () => {
it('returns a list of changed opened files', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('changed'));
localState.openFiles[1].changed = true;
const changedFiles = getters.changedFiles(localState);
expect(changedFiles.length).toBe(1);
expect(changedFiles[0].name).toBe('changed');
});
});
describe('activeFile', () => {
it('returns the current active file', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
localState.openFiles[1].active = true;
expect(getters.activeFile(localState).name).toBe('active');
});
it('returns undefined if no active files are found', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
expect(getters.activeFile(localState)).toBeNull();
});
});
describe('activeFileExtension', () => {
it('returns the file extension for the current active file', () => {
localState.openFiles.push(file('active'));
localState.openFiles[0].active = true;
localState.openFiles[0].path = 'test.js';
expect(getters.activeFileExtension(localState)).toBe('.js');
localState.openFiles[0].path = 'test.es6.js';
expect(getters.activeFileExtension(localState)).toBe('.js');
});
});
describe('canEditFile', () => {
beforeEach(() => {
localState.onTopOfBranch = true;
localState.canCommit = true;
localState.openFiles.push(file());
localState.openFiles[0].active = true;
});
it('returns true if user can commit and has open files', () => {
expect(getters.canEditFile(localState)).toBeTruthy();
});
it('returns false if user can commit and has no open files', () => {
localState.openFiles = [];
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user can commit and active file is binary', () => {
localState.openFiles[0].binary = true;
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user cant commit', () => {
localState.canCommit = false;
expect(getters.canEditFile(localState)).toBeFalsy();
});
});
describe('modifiedFiles', () => {
it('returns a list of modified files', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('changed'));
localState.openFiles[1].changed = true;
const modifiedFiles = getters.modifiedFiles(localState);
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed');
});
});
describe('addedFiles', () => {
it('returns a list of added files', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('added'));
localState.openFiles[1].changed = true;
localState.openFiles[1].tempFile = true;
const modifiedFiles = getters.addedFiles(localState);
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('added');
});
});
});
import mutations from '~/ide/stores/mutations/branch';
import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('SET_CURRENT_BRANCH', () => {
it('sets currentBranch', () => {
mutations.SET_CURRENT_BRANCH(localState, 'master');
expect(localState.currentBranchId).toBe('master');
});
});
});
import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store file mutations', () => {
let localState;
let localFile;
beforeEach(() => {
localState = state();
localFile = file();
});
describe('SET_FILE_ACTIVE', () => {
it('sets the file active', () => {
mutations.SET_FILE_ACTIVE(localState, {
file: localFile,
active: true,
});
expect(localFile.active).toBeTruthy();
});
});
describe('TOGGLE_FILE_OPEN', () => {
beforeEach(() => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
});
it('adds into opened files', () => {
expect(localFile.opened).toBeTruthy();
expect(localState.openFiles.length).toBe(1);
});
it('removes from opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
expect(localFile.opened).toBeFalsy();
expect(localState.openFiles.length).toBe(0);
});
});
describe('SET_FILE_DATA', () => {
it('sets extra file data', () => {
mutations.SET_FILE_DATA(localState, {
data: {
blame_path: 'blame',
commits_path: 'commits',
permalink: 'permalink',
raw_path: 'raw',
binary: true,
html: 'html',
render_error: 'render_error',
},
file: localFile,
});
expect(localFile.blamePath).toBe('blame');
expect(localFile.commitsPath).toBe('commits');
expect(localFile.permalink).toBe('permalink');
expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy();
expect(localFile.html).toBe('html');
expect(localFile.renderError).toBe('render_error');
});
});
describe('SET_FILE_RAW_DATA', () => {
it('sets raw data', () => {
mutations.SET_FILE_RAW_DATA(localState, {
file: localFile,
raw: 'testing',
});
expect(localFile.raw).toBe('testing');
});
});
describe('UPDATE_FILE_CONTENT', () => {
beforeEach(() => {
localFile.raw = 'test';
});
it('sets content', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
content: 'test',
});
expect(localFile.content).toBe('test');
});
it('sets changed if content does not match raw', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
content: 'testing',
});
expect(localFile.content).toBe('testing');
expect(localFile.changed).toBeTruthy();
});
});
describe('DISCARD_FILE_CHANGES', () => {
beforeEach(() => {
localFile.content = 'test';
localFile.changed = true;
});
it('resets content and changed', () => {
mutations.DISCARD_FILE_CHANGES(localState, localFile);
expect(localFile.content).toBe('');
expect(localFile.changed).toBeFalsy();
});
});
describe('CREATE_TMP_FILE', () => {
it('adds file into parent tree', () => {
const f = file('tmpFile');
mutations.CREATE_TMP_FILE(localState, {
file: f,
parent: localFile,
});
expect(localFile.tree.length).toBe(1);
expect(localFile.tree[0].name).toBe(f.name);
});
});
});
import mutations from '~/ide/stores/mutations/tree';
import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store tree mutations', () => {
let localState;
let localTree;
beforeEach(() => {
localState = state();
localTree = file();
});
describe('TOGGLE_TREE_OPEN', () => {
it('toggles tree open', () => {
mutations.TOGGLE_TREE_OPEN(localState, localTree);
expect(localTree.opened).toBeTruthy();
mutations.TOGGLE_TREE_OPEN(localState, localTree);
expect(localTree.opened).toBeFalsy();
});
});
describe('SET_DIRECTORY_DATA', () => {
const data = [{
name: 'tree',
},
{
name: 'submodule',
},
{
name: 'blob',
}];
it('adds directory data', () => {
mutations.SET_DIRECTORY_DATA(localState, {
data,
tree: localState,
});
expect(localState.tree.length).toBe(3);
expect(localState.tree[0].name).toBe('tree');
expect(localState.tree[1].name).toBe('submodule');
expect(localState.tree[2].name).toBe('blob');
});
});
describe('SET_PARENT_TREE_URL', () => {
it('sets the parent tree url', () => {
mutations.SET_PARENT_TREE_URL(localState, 'test');
expect(localState.parentTreeUrl).toBe('test');
});
});
describe('CREATE_TMP_TREE', () => {
it('adds tree into parent tree', () => {
const tmpEntry = file('tmpTree');
mutations.CREATE_TMP_TREE(localState, {
tmpEntry,
parent: localTree,
});
expect(localTree.tree.length).toBe(1);
expect(localTree.tree[0].name).toBe(tmpEntry.name);
});
});
});
import mutations from '~/ide/stores/mutations';
import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store mutations', () => {
let localState;
let entry;
beforeEach(() => {
localState = state();
entry = file();
});
describe('SET_INITIAL_DATA', () => {
it('sets all initial data', () => {
mutations.SET_INITIAL_DATA(localState, {
test: 'test',
});
expect(localState.test).toBe('test');
});
});
describe('SET_PREVIEW_MODE', () => {
it('sets currentBlobView to repo-preview', () => {
mutations.SET_PREVIEW_MODE(localState);
expect(localState.currentBlobView).toBe('repo-preview');
localState.currentBlobView = 'testing';
mutations.SET_PREVIEW_MODE(localState);
expect(localState.currentBlobView).toBe('repo-preview');
});
});
describe('SET_EDIT_MODE', () => {
it('sets currentBlobView to repo-editor', () => {
mutations.SET_EDIT_MODE(localState);
expect(localState.currentBlobView).toBe('repo-editor');
localState.currentBlobView = 'testing';
mutations.SET_EDIT_MODE(localState);
expect(localState.currentBlobView).toBe('repo-editor');
});
});
describe('TOGGLE_LOADING', () => {
it('toggles loading of entry', () => {
mutations.TOGGLE_LOADING(localState, entry);
expect(entry.loading).toBeTruthy();
mutations.TOGGLE_LOADING(localState, entry);
expect(entry.loading).toBeFalsy();
});
});
describe('TOGGLE_EDIT_MODE', () => {
it('toggles editMode', () => {
mutations.TOGGLE_EDIT_MODE(localState);
expect(localState.editMode).toBeFalsy();
mutations.TOGGLE_EDIT_MODE(localState);
expect(localState.editMode).toBeTruthy();
});
});
describe('TOGGLE_DISCARD_POPUP', () => {
it('sets discardPopupOpen', () => {
mutations.TOGGLE_DISCARD_POPUP(localState, true);
expect(localState.discardPopupOpen).toBeTruthy();
mutations.TOGGLE_DISCARD_POPUP(localState, false);
expect(localState.discardPopupOpen).toBeFalsy();
});
});
describe('SET_ROOT', () => {
it('sets isRoot & initialRoot', () => {
mutations.SET_ROOT(localState, true);
expect(localState.isRoot).toBeTruthy();
expect(localState.isInitialRoot).toBeTruthy();
mutations.SET_ROOT(localState, false);
expect(localState.isRoot).toBeFalsy();
expect(localState.isInitialRoot).toBeFalsy();
});
});
describe('SET_LEFT_PANEL_COLLAPSED', () => {
it('sets left panel collapsed', () => {
mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
expect(localState.leftPanelCollapsed).toBeTruthy();
mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
expect(localState.leftPanelCollapsed).toBeFalsy();
});
});
describe('SET_RIGHT_PANEL_COLLAPSED', () => {
it('sets right panel collapsed', () => {
mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
expect(localState.rightPanelCollapsed).toBeTruthy();
mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
expect(localState.rightPanelCollapsed).toBeFalsy();
});
});
});
import * as utils from '~/ide/stores/utils';
import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store utils', () => {
describe('setPageTitle', () => {
it('sets the document page title', () => {
utils.setPageTitle('test');
expect(document.title).toBe('test');
});
});
describe('treeList', () => {
let localState;
beforeEach(() => {
localState = state();
});
it('returns flat tree list', () => {
localState.trees = [];
localState.trees['abcproject/mybranch'] = {
tree: [],
};
const baseTree = localState.trees['abcproject/mybranch'].tree;
baseTree.push(file('1'));
baseTree[0].tree.push(file('2'));
baseTree[0].tree[0].tree.push(file('3'));
const treeList = utils.treeList(localState, 'abcproject/mybranch');
expect(treeList.length).toBe(3);
expect(treeList[1].name).toBe(baseTree[0].tree[0].name);
expect(treeList[2].name).toBe(baseTree[0].tree[0].tree[0].name);
});
});
describe('createTemp', () => {
it('creates temp tree', () => {
const tmp = utils.createTemp({
name: 'test',
path: 'test',
type: 'tree',
level: 0,
changed: false,
content: '',
base64: '',
});
expect(tmp.tempFile).toBeTruthy();
expect(tmp.icon).toBe('fa-folder');
});
it('creates temp file', () => {
const tmp = utils.createTemp({
name: 'test',
path: 'test',
type: 'blob',
level: 0,
changed: false,
content: '',
base64: '',
});
expect(tmp.tempFile).toBeTruthy();
expect(tmp.icon).toBe('fa-file-text-o');
});
});
describe('findIndexOfFile', () => {
let localState;
beforeEach(() => {
localState = [{
path: '1',
}, {
path: '2',
}];
});
it('finds in the index of an entry by path', () => {
const index = utils.findIndexOfFile(localState, {
path: '2',
});
expect(index).toBe(1);
});
});
describe('findEntry', () => {
let localState;
beforeEach(() => {
localState = {
tree: [{
type: 'tree',
name: 'test',
}, {
type: 'blob',
name: 'file',
}],
};
});
it('returns an entry found by name', () => {
const foundEntry = utils.findEntry(localState.tree, 'tree', 'test');
expect(foundEntry.type).toBe('tree');
expect(foundEntry.name).toBe('test');
});
it('returns undefined when no entry found', () => {
const foundEntry = utils.findEntry(localState.tree, 'blob', 'test');
expect(foundEntry).toBeUndefined();
});
});
});
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