Commit 3551d6a3 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'ide-code-cleanup' into 'master'

Tidy up IDE code

See merge request gitlab-org/gitlab-ee!4951
parents a258b730 79137a95
......@@ -68,7 +68,6 @@
.ide-new-btn {
display: none;
margin-top: -4px;
margin-bottom: -4px;
margin-right: -8px;
}
......@@ -84,7 +83,6 @@
fill: $gl-text-color-secondary;
}
}
}
a {
......@@ -290,7 +288,7 @@
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: .4;
opacity: 0.4;
}
}
}
......@@ -548,7 +546,6 @@
height: 10px;
margin-left: 3px;
}
}
.multi-file-commit-list-path {
......@@ -626,7 +623,7 @@
top: 0;
width: 100px;
height: 1px;
background-color: rgba($red-500, .5);
background-color: rgba($red-500, 0.5);
}
}
}
......@@ -720,12 +717,13 @@
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
height: calc(
100vh - #{$header-height + $performance-bar-height + $flash-height}
);
}
}
}
.dragHandle {
position: absolute;
top: 0;
......
......@@ -52,7 +52,7 @@
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file)"
@click="discardFileChanges(file.path)"
>
Discard
</button>
......
......@@ -5,7 +5,6 @@
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 {
......@@ -16,7 +15,6 @@
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
props: {
emptyStateSvgPath: {
......@@ -33,18 +31,12 @@
},
},
computed: {
...mapState([
'currentBlobView',
'selectedFile',
'changedFiles',
]),
...mapGetters([
'activeFile',
]),
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
......@@ -67,20 +59,29 @@
<template
v-if="activeFile"
>
<repo-tabs/>
<component
<repo-tabs
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
/>
<repo-editor
class="multi-file-edit-pane-content"
:is="currentBlobView"
:file="activeFile"
/>
<repo-file-buttons
:file="activeFile"
/>
<repo-file-buttons />
<ide-status-bar
:file="selectedFile"
:file="activeFile"
/>
</template>
<template
v-else
>
<div class="ide-empty-state">
<div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
......
<script>
import { 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';
import { mapActions, mapGetters, mapState } 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';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
data() {
return {
width: 340,
};
},
computed: {
...mapState([
'rightPanelCollapsed',
'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,
});
},
toggleFullbarCollapsed() {
if (this.rightPanelCollapsed) {
this.toggleCollapsed();
}
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
......@@ -104,7 +64,10 @@
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="toggleCollapsed"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
......@@ -117,15 +80,5 @@
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="340"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"
/>
</div>
</resizable-panel>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
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,
export default {
components: {
repoTree,
icon,
newDropdown,
},
branch: {
type: Object,
required: true,
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
},
};
};
</script>
<template>
......@@ -40,8 +40,8 @@ export default {
/>
</div>
</div>
<div>
<repo-tree :tree-id="branch.treeId" />
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import repoFile from './repo_file.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
export default {
components: {
repoFile,
skeletonLoadingContainer,
RepoFile,
SkeletonLoadingContainer,
},
props: {
treeId: {
type: String,
tree: {
type: Object,
required: true,
},
},
computed: {
...mapState([
'trees',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
selctedTree() {
return this.trees[this.treeId].tree;
},
showLoading() {
return !this.trees[this.treeId] || this.trees[this.treeId].loading;
},
},
};
</script>
<template>
<div
class="ide-file-list"
v-if="treeId"
>
<template v-if="showLoading">
<template v-if="tree.loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -47,10 +29,13 @@ export default {
<skeleton-loading-container />
</div>
</template>
<repo-file
v-for="file in selctedTree"
:key="file.key"
:file="file"
/>
<template v-else>
<repo-file
v-for="file in tree.tree"
:key="file.key"
:file="file"
:level="0"
/>
</template>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapGetters } 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';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
......@@ -11,65 +12,27 @@
icon,
panelResizer,
skeletonLoadingContainer,
},
data() {
return {
width: 290,
};
ResizablePanel,
},
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',
...mapGetters([
'projectsWithTrees',
]),
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"
<resizable-panel
:collapsible="false"
:initial-width="290"
side="left"
>
<div class="multi-file-commit-panel-inner">
<template v-if="showLoading">
<template v-if="loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -79,36 +42,10 @@
</div>
</template>
<project-tree
v-for="project in projects"
v-for="project in projectsWithTrees"
: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>
</resizable-panel>
</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';
......@@ -20,11 +19,6 @@
required: true,
},
},
computed: {
...mapState([
'selectedFile',
]),
},
};
</script>
......@@ -35,32 +29,32 @@
name="branch"
:size="12"
/>
{{ selectedFile.branchId }}
{{ file.branchId }}
</div>
<div>
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url"
:title="file.lastCommit.message"
:href="file.lastCommit.url"
>
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
{{ timeFormated(file.lastCommit.updatedAt) }} by
{{ file.lastCommit.author }}
</a>
</div>
</div>
<div class="text-right">
{{ selectedFile.name }}
{{ file.name }}
</div>
<div class="text-right">
{{ selectedFile.eol }}
{{ file.eol }}
</div>
<div class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
{{ selectedFile.fileLanguage }}
{{ file.fileLanguage }}
</div>
</div>
</template>
<script>
import $ from 'jquery';
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 { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
......@@ -18,10 +19,6 @@
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
data() {
return {
......@@ -31,6 +28,9 @@
};
},
methods: {
...mapActions([
'createTempEntry',
]),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
......@@ -85,7 +85,7 @@
<upload
:branch-id="branch"
:path="path"
:parent="parent"
@create="createTempEntry"
/>
</li>
<li>
......@@ -104,8 +104,8 @@
:type="modalType"
:branch-id="branch"
:path="path"
:parent="parent"
@hide="hideModal"
@create="createTempEntry"
/>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import modal from '~/vue_shared/components/modal.vue';
......@@ -12,10 +11,6 @@
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: {
type: String,
required: true,
......@@ -31,9 +26,6 @@
};
},
computed: {
...mapState([
'currentProjectId',
]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
......@@ -60,15 +52,10 @@
this.$refs.fieldName.focus();
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
this.$emit('create', {
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
name: this.entryName,
type: this.type,
});
......
<script>
import { mapActions, mapState } from 'vuex';
export default {
props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
path: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
......@@ -25,9 +18,6 @@
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
......@@ -36,11 +26,9 @@
result = result.split('base64,')[1];
}
this.createTempEntry({
name,
projectId: this.currentProjectId,
this.$emit('create', {
name: `${(this.path ? `${this.path}/` : '')}${name}`,
branchId: this.branchId,
parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
......
......@@ -53,7 +53,6 @@ export default {
},
methods: {
...mapActions([
'getTreeData',
'setPanelCollapsedStatus',
]),
...mapActions('commit', [
......
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import modal from '~/vue_shared/components/modal.vue';
export default {
components: {
modal,
},
computed: {
...mapState([
'editMode',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
},
methods: {
...mapActions([
'toggleEditMode',
]),
},
};
</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>
</div>
</template>
<script>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
return this.file && this.file.binary && !this.file.raw;
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
file(oldVal, newVal) {
if (newVal.path !== this.file.path) {
this.initMonaco();
}
},
......@@ -34,11 +35,6 @@ export default {
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
viewer() {
this.createEditorInstance();
},
......@@ -72,7 +68,7 @@ export default {
this.editor.clearEditor();
this.getRawFileData(this.activeFile)
this.getRawFileData(this.file)
.then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
......@@ -101,9 +97,9 @@ export default {
});
},
setupEditor() {
if (!this.activeFile || !this.editor.instance) return;
if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.activeFile);
this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model);
......@@ -112,7 +108,7 @@ export default {
if (file.active) {
this.changeFileContent({
file,
path: file.path,
content: model.getModel().getValue(),
});
}
......@@ -127,8 +123,8 @@ export default {
});
this.editor.setPosition({
lineNumber: this.activeFile.editorRow,
column: this.activeFile.editorColumn,
lineNumber: this.file.editorRow,
column: this.file.editorColumn,
});
// Handle File Language
......@@ -152,7 +148,7 @@ export default {
>
<div
v-if="shouldHideEditor"
v-html="activeFile.html"
v-html="file.html"
>
</div>
<div
......
<script>
import { mapActions, mapState } from 'vuex';
import { mapActions } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
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';
import fileStatusIcon from 'ee/ide/components/repo_file_status_icon.vue'; // eslint-disable-line import/first
import changedFileIcon from 'ee/ide/components/changed_file_icon.vue'; // eslint-disable-line import/first
export default {
name: 'RepoFile',
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
changedFileIcon,
export default {
name: 'RepoFile',
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
changedFileIcon,
},
props: {
file: {
type: Object,
required: true,
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
showExtraColumns: {
type: Boolean,
default: false,
},
level: {
type: Number,
required: true,
},
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);
},
fileClass() {
if (this.file.type === 'blob') {
if (this.file.active) {
return 'file-open file-active';
}
return this.file.opened ? 'file-open' : '';
} else if (this.file.type === 'tree') {
return 'folder';
}
return '';
},
},
computed: {
isTree() {
return this.file.type === 'tree';
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
isBlob() {
return this.file.type === 'blob';
},
levelIndentation() {
return {
marginLeft: `${this.level * 16}px`,
};
},
methods: {
...mapActions([
'updateDelayViewerUpdated',
]),
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,
});
}
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
'file-active': this.isBlob && this.file.active,
folder: this.isTree,
};
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path);
}
const delayPromise = this.file.changed ?
Promise.resolve() : this.updateDelayViewerUpdated(true);
const delayPromise = this.file.changed
? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
this.$router.push(`/project${row.url}`);
});
},
return delayPromise.then(() => {
router.push(`/project${this.file.url}`);
});
},
};
},
};
</script>
<template>
......@@ -101,54 +82,45 @@
>
<div
class="file-name"
@click="clickFile(file)"
@click="clickFile"
role="button"
>
<a
<span
class="ide-file-name str-truncated"
:style="levelIndentation"
>
<file-icon
:file-name="file.name"
:loading="file.loading"
:folder="file.type === 'tree'"
:folder="isTree"
:opened="file.opened"
:style="levelIndentation"
:size="16"
/>
{{ file.name }}
<file-status-icon :file="file" />
</a>
<file-status-icon
:file="file"
/>
</span>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5 pull-right"
/>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
:parent="file"
/>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5"
class="pull-right prepend-left-8"
/>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@click.stop
:href="file.tree_url"
>
{{ shortId }}
</a>
</span>
</template>
</div>
</div>
<template
v-if="file.opened"
>
<template v-if="file.opened">
<repo-file
v-for="childFile in file.tree"
:key="childFile.key"
:file="childFile"
:level="level + 1"
/>
</template>
</div>
......
<script>
import { mapGetters } from 'vuex';
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
return this.file.rawPath ||
this.file.blamePath ||
this.file.commitsPath ||
this.file.permalink;
},
rawDownloadButtonLabel() {
return this.activeFile.binary ? 'Download' : 'Raw';
return this.file.binary ? 'Download' : 'Raw';
},
},
};
......@@ -25,7 +26,7 @@ export default {
class="multi-file-editor-btn-group"
>
<a
:href="activeFile.rawPath"
:href="file.rawPath"
target="_blank"
class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
......@@ -38,19 +39,19 @@ export default {
aria-label="File actions"
>
<a
:href="activeFile.blamePath"
:href="file.blamePath"
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="activeFile.commitsPath"
:href="file.commitsPath"
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="activeFile.permalink"
:href="file.permalink"
class="btn btn-default btn-sm permalink"
>
Permalink
......
<script>
import $ from 'jquery';
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>
......@@ -67,7 +67,7 @@
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
@click.stop.prevent="closeFile(tab.path)"
:aria-label="closeLabel"
>
<icon
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
......@@ -8,29 +8,33 @@
RepoTab,
EditorMode,
},
props: {
files: {
type: Array,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
},
data() {
return {
showShadow: false,
};
},
computed: {
...mapGetters([
'hasChanges',
]),
...mapState([
'openFiles',
'viewer',
]),
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions([
'updateViewer',
]),
...mapActions(['updateViewer']),
},
};
</script>
......@@ -42,7 +46,7 @@
ref="tabsScroller"
>
<repo-tab
v-for="tab in openFiles"
v-for="tab in files"
:key="tab.key"
:tab="tab"
/>
......
<script>
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default {
components: {
PanelResizer,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
minSize: {
type: Number,
required: false,
default: 200,
},
side: {
type: String,
required: true,
},
},
data() {
return {
width: this.initialWidth,
};
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
},
maxSize: (window.innerWidth / 2),
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed && collapsible,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
>
<slot></slot>
<panel-resizer
:size.sync="width"
:enabled="!collapsed"
:start-size="initialWidth"
:min-size="minSize"
:max-size="$options.maxSize"
@resize-start="setResizingStatus(true)"
@resize-end="setResizingStatus(false)"
:side="side === 'right' ? 'left' : 'right'"
/>
</div>
</template>
......@@ -2,9 +2,6 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter);
......@@ -76,7 +73,7 @@ router.beforeEach((to, from, next) => {
})
.then(() => {
if (to.params[0]) {
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
const treeEntry = store.state.entries[to.params[0]];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
......
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -8,11 +10,11 @@ export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
state.changedFiles.forEach(file => {
commit(types.DISCARD_FILE_CHANGES, file.path);
if (file.tempFile) {
dispatch('closeFile', file);
dispatch('closeFile', file.path);
}
});
......@@ -20,20 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file));
};
export const toggleEditMode = ({ commit, dispatch }) => {
commit(types.TOGGLE_EDIT_MODE);
dispatch('toggleBlobView');
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
state.openFiles.forEach(file => dispatch('closeFile', file.path));
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
......@@ -49,28 +38,63 @@ export const setResizingStatus = ({ commit }, resizing) => {
};
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,
{ state, commit, dispatch },
{ branchId, name, type, content = '', base64 = false },
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
const fullName =
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
`The name "${name
.split('/')
.pop()}" is already taken in this directory.`,
'alert',
document,
null,
false,
true,
);
resolve();
return null;
}
worker.addEventListener('message', ({ data }) => {
const { file } = data;
worker.terminate();
commit(types.CREATE_TMP_ENTRY, {
data,
projectId: state.currentProjectId,
branchId,
});
if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
}
resolve(file);
});
} else if (type === 'blob') {
dispatch('createTempFile', {
projectId,
worker.postMessage({
data: [fullName],
projectId: state.currentProjectId,
branchId,
parent: selectedParent,
name,
type,
tempFile: true,
base64,
content,
});
}
};
return null;
});
export const scrollToTab = () => {
Vue.nextTick(() => {
......@@ -95,4 +119,3 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/branch';
import flash from '~/flash';
import service from '../../services';
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);
});
......@@ -4,49 +4,44 @@ import eventHub from 'ee/ide/eventhub';
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) => {
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
import { setPageTitle } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
const file = state.entries[path];
const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false });
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.openFiles[nextIndexToOpen];
const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
router.push(`/project${nextFileToOpen.url}`);
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
const file = state.entries[path];
const currentActiveFile = getters.activeFile;
if (file.active) return;
if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
commit(types.SET_FILE_ACTIVE, {
path: currentActiveFile.path,
active: false,
});
}
commit(types.SET_FILE_ACTIVE, { file, active: true });
commit(types.SET_FILE_ACTIVE, { path, 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);
};
......@@ -54,104 +49,97 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file });
service.getFileData(file.url)
.then((res) => {
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
});
};
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ state, commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
export const getRawFileData = ({ commit, dispatch }, file) =>
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) {
commit(types.ADD_FILE_TO_CHANGED, file);
commit(types.ADD_FILE_TO_CHANGED, path);
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, file);
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
if (getters.activeFile) {
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
export const setFileEOL = ({ getters, commit }, { eol }) => {
if (getters.activeFile) {
commit(types.SET_FILE_EOL, { file: getters.activeFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
export const setEditorPosition = (
{ getters, commit },
{ editorRow, editorColumn },
) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
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);
commit(types.ADD_FILE_TO_CHANGED, file);
dispatch('setFileActive', file);
if (!state.editMode && !file.base64) {
dispatch('toggleEditMode', true);
}
router.push(`/project${file.url}`);
return Promise.resolve(file);
};
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
export const discardFileChanges = ({ commit }, file) => {
commit(types.DISCARD_FILE_CHANGES, file);
commit(types.REMOVE_FILE_FROM_CHANGED, file);
commit(types.DISCARD_FILE_CHANGES, path);
commit(types.REMOVE_FILE_FROM_CHANGED, path);
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.TOGGLE_FILE_OPEN, path);
}
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
......
......@@ -2,7 +2,6 @@ import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
......@@ -25,3 +24,26 @@ export const getProjectData = (
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
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]);
}
});
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,
sortTree,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
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, { entry: 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) => {
dispatch('updateDirectoryData', { data, tree, projectId, branch, clearTree: false });
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, { entry: 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, { entry: tree });
reject(e);
});
} else {
resolve();
}
}
});
export const toggleTreeOpen = ({ commit, dispatch }, { tree }) => {
commit(types.TOGGLE_TREE_OPEN, tree);
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
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, { entry: row });
visitUrl(row.url);
dispatch('toggleTreeOpen', row.path);
} else if (row.type === 'blob' && (row.opened || row.changed)) {
if (row.changed && !row.opened) {
commit(types.TOGGLE_FILE_OPEN, row);
commit(types.TOGGLE_FILE_OPEN, row.path);
}
dispatch('setFileActive', row);
dispatch('setFileActive', row.path);
} 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;
......@@ -146,47 +50,6 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
{ commit, state },
{ data, tree, projectId, branch, clearTree = true },
) => {
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({
projectId: `${projectId}`,
branchId: branch,
entry,
level,
type,
parentTreeUrl,
state,
});
let formattedData = [
...data.trees.map(t => createEntry(t, 'tree')),
...data.submodules.map(m => createEntry(m, 'submodule')),
...data.blobs.map(b => createEntry(b, 'blob')),
];
if (!clearTree && tree) {
const tempFiles = state.changedFiles.filter(f => f.tempFile && f.path === `${tree.path}/${f.name}`);
if (tempFiles.length) {
formattedData = formattedData.concat(tempFiles);
}
}
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
};
export const getFiles = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
......@@ -199,75 +62,25 @@ export const getFiles = (
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
const newTree = data.reduce((outputArray, file) => {
const pathSplit = file.split('/');
const blobName = pathSplit.pop();
let selectedFolderTree;
let foundFolder = null;
let fullPath = '';
if (pathSplit.length > 0) {
const newBaseFolders = pathSplit.reduce((newFolders, folder, currentIndex) => {
fullPath += `/${folder}`;
foundFolder = findEntry(selectedFolderTree || outputArray, 'tree', fullPath, 'path');
if (!foundFolder) {
foundFolder = createOrMergeEntry({
projectId,
branchId,
entry: {
id: fullPath,
name: folder,
path: fullPath,
url: `/${projectId}/tree/${branchId}/${fullPath}`,
},
level: currentIndex,
type: 'tree',
parentTreeUrl: '',
state,
});
if (selectedFolderTree) {
selectedFolderTree.push(foundFolder);
} else {
newFolders.push(foundFolder);
}
}
selectedFolderTree = foundFolder.tree;
return newFolders;
}, []);
if (newBaseFolders.length) outputArray.push(newBaseFolders[0]);
}
// Add file
const blobEntry = createOrMergeEntry({
projectId,
branchId,
entry: {
id: file,
name: blobName,
path: file,
url: `/${projectId}/blob/${branchId}/${file}`,
},
level: foundFolder ? (foundFolder.level + 1) : 0,
type: 'blob',
parentTreeUrl: foundFolder ? foundFolder.url : '',
state,
});
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
if (selectedFolderTree) {
selectedFolderTree.push(blobEntry);
} else {
outputArray.push(blobEntry);
}
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
return outputArray;
}, []);
worker.terminate();
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: sortTree(newTree) });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
resolve();
});
resolve();
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
......
export const activeFile = state => state.openFiles.find(file => file.active) || null;
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 addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
export const modifiedFiles = state =>
state.changedFiles.filter(f => !f.tempFile);
return state.canCommit &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
const project = state.projects[projectId];
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
return {
...project,
branches: Object.keys(project.branches).map(branchId => {
const branch = project.branches[branchId];
return {
...branch,
tree: state.trees[branch.treeId],
};
}),
};
});
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
......@@ -137,6 +137,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch(
......
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_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
......@@ -20,7 +19,6 @@ 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';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
......@@ -35,17 +33,11 @@ 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';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
// 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 SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......@@ -8,25 +8,19 @@ 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, forceValue = undefined }) {
Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading,
});
},
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading,
});
}
},
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
......@@ -57,6 +51,44 @@ export default {
lastCommitMsg,
});
},
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
});
},
[types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
Object.keys(data.entries).reduce((acc, key) => {
const entry = data.entries[key];
const foundEntry = state.entries[key];
if (!foundEntry) {
Object.assign(state.entries, {
[key]: entry,
});
} else {
const tree = entry.tree.filter(
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree),
});
}
return acc.concat(key);
}, []);
const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
e => e.path === data.treeList[0].path,
);
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
data.treeList,
),
});
}
},
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
......
......@@ -7,16 +7,14 @@ export default {
});
},
[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,
[branchName]: {
...branch,
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
},
},
});
},
......
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
Object.assign(file, {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
});
Object.assign(state, {
selectedFile: file,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
opened: !file.opened,
[types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], {
opened: !state.entries[path].opened,
});
if (file.opened) {
state.openFiles.push(file);
if (state.entries[path].opened) {
state.openFiles.push(state.entries[path]);
} else {
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path),
});
}
},
[types.SET_FILE_DATA](state, { data, file }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
id: data.id,
blamePath: data.blame_path,
commitsPath: data.commits_path,
......@@ -34,53 +31,52 @@ export default {
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
Object.assign(file, {
Object.assign(state.entries[path], {
content,
changed,
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
fileLanguage,
});
},
[types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
eol,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
editorRow,
editorColumn,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: file.raw,
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
changed: false,
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
},
[types.ADD_FILE_TO_CHANGED](state, file) {
state.changedFiles.push(file);
[types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, {
changedFiles: state.changedFiles.concat(state.entries[path]),
});
},
[types.REMOVE_FILE_FROM_CHANGED](state, file) {
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
state.changedFiles.splice(indexOfChangedFile, 1);
[types.REMOVE_FILE_FROM_CHANGED](state, path) {
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
changed,
});
},
......
import * as types from '../mutation_types';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
Object.assign(tree, {
opened: !tree.opened,
[types.TOGGLE_TREE_OPEN](state, path) {
Object.assign(state.entries[path], {
opened: !state.entries[path].opened,
});
},
[types.CREATE_TREE](state, { treePath }) {
......@@ -16,14 +16,13 @@ export default {
}),
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
});
},
[types.SET_PARENT_TREE_URL](state, url) {
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(state, {
parentTreeUrl: url,
trees: Object.assign(state.trees, {
[treePath]: {
tree: data,
},
}),
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
......@@ -31,9 +30,6 @@ export default {
lastCommitPath: url,
});
},
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
[types.REMOVE_ALL_CHANGES_FILES](state) {
Object.assign(state, {
changedFiles: [],
......
export default () => ({
canCommit: false,
currentProjectId: '',
currentBranchId: '',
currentBlobView: 'repo-editor',
changedFiles: [],
editMode: true,
endpoints: {},
isInitialRoot: false,
lastCommitMsg: '',
lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],
selectedFile: null,
path: '',
parentTreeUrl: '',
trees: {},
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
});
import _ from 'underscore';
export const dataStructure = () => ({
id: '',
key: '',
......@@ -9,9 +7,7 @@ export const dataStructure = () => ({
name: '',
url: '',
path: '',
level: 0,
tempFile: false,
icon: '',
tree: [],
loading: false,
opened: false,
......@@ -25,7 +21,6 @@ export const dataStructure = () => ({
updatedAt: '',
author: '',
},
tree_url: '',
blamePath: '',
commitsPath: '',
permalink: '',
......@@ -51,8 +46,6 @@ export const decorateData = (entity) => {
type,
url,
name,
icon,
tree_url,
path,
renderError,
content = '',
......@@ -61,7 +54,6 @@ export const decorateData = (entity) => {
opened = false,
changed = false,
parentTreeUrl = '',
level = 0,
base64 = false,
file_lock,
......@@ -77,11 +69,8 @@ export const decorateData = (entity) => {
type,
name,
url,
tree_url,
path,
level,
tempFile,
icon: `fa-${icon}`,
opened,
active,
parentTreeUrl,
......@@ -95,32 +84,6 @@ export const decorateData = (entity) => {
};
};
/*
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, prop = 'name') => tree.find(
f => f.type === type && f[prop] === name,
);
......@@ -131,63 +94,6 @@ 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 = ({ projectId,
branchId,
entry,
type,
parentTreeUrl,
level,
state }) => {
if (state.changedFiles.length) {
const foundChangedFile = findEntry(state.changedFiles, type, entry.path, 'path');
if (foundChangedFile) {
return foundChangedFile;
}
}
if (state.openFiles.length) {
const foundOpenFile = findEntry(state.openFiles, type, entry.path, 'path');
if (foundOpenFile) {
return foundOpenFile;
}
}
return decorateData({
...entry,
projectId,
branchId,
type,
parentTreeUrl,
level,
});
};
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
......@@ -214,11 +120,6 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
export const sortTree = (sortedTree) => {
sortedTree.forEach((el) => {
Object.assign(el, {
tree: el && el.tree ? sortTree(el.tree) : [],
});
});
return sortedTree.sort(sortTreesByTypeAndName);
};
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
})).sort(sortTreesByTypeAndName);
import {
decorateData,
sortTree,
} from '../utils';
self.addEventListener('message', (e) => {
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${(parentFolder ? `${parentFolder.path}/` : '')}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
const tree = decorateData({
projectId,
branchId,
id: folderPath,
name: folderName,
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}`,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
});
Object.assign(acc, {
[folderPath]: tree,
});
if (parentFolder) {
parentFolder.tree.push(tree);
} else {
treeList.push(tree);
}
pathAcc.push(tree.path);
} else {
pathAcc.push(foundEntry.path);
}
return pathAcc;
}, []);
}
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
});
Object.assign(acc, {
[path]: file,
});
if (fileFolder) {
fileFolder.tree.push(file);
} else {
treeList.push(file);
}
}
return acc;
}, {});
self.postMessage({
entries,
treeList: sortTree(treeList),
file,
});
});
......@@ -33,42 +33,5 @@ describe('Multi-file editor right context bar', () => {
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 sidebar collapses the bar', () => {
spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve());
vm.$el.querySelector('.multi-file-commit-panel-section').click();
expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({
side: 'right',
collapsed: false,
});
});
});
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,
});
});
it('when expanded clicking the main sidebar is not collapsing the bar', () => {
spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve());
vm.$el.querySelector('.multi-file-commit-panel-section').click();
expect(vm.setPanelCollapsedStatus).not.toHaveBeenCalledWith({
side: 'right',
collapsed: false,
});
});
});
import Vue from 'vue';
import store from 'ee/ide/stores';
import ideRepoTree from 'ee/ide/components/ide_repo_tree.vue';
import { file, resetStore } from '../helpers';
import createComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('IdeRepoTree', () => {
let vm;
let tree;
beforeEach(() => {
const IdeRepoTree = Vue.extend(ideRepoTree);
vm = new IdeRepoTree({
store,
propsData: {
treeId: 'abcproject/mybranch',
},
});
vm.$store.state.currentBranch = 'master';
vm.$store.state.trees['abcproject/mybranch'] = {
tree = {
tree: [file()],
loading: false,
};
vm.$mount();
vm = createComponent(IdeRepoTree, {
tree,
});
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a sidebar', () => {
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(vm.$el.querySelector('.repo-file-options')).toBeFalsy();
expect(vm.$el.querySelector('.loading-file')).toBeFalsy();
expect(vm.$el.querySelector('.file')).toBeTruthy();
expect(vm.$el.querySelector('.loading-file')).toBeNull();
expect(vm.$el.querySelector('.file')).not.toBeNull();
});
it('renders 3 loading files if tree is loading', (done) => {
vm.$store.state.trees['123'] = {
tree: [],
loading: true,
};
vm.treeId = '123';
tree.loading = true;
Vue.nextTick(() => {
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3);
done();
......
......@@ -11,8 +11,6 @@ describe('IdeSidebar', () => {
const Component = Vue.extend(ideSidebar);
vm = createComponentWithStore(Component, store).$mount();
vm.$store.state.leftPanelCollapsed = false;
});
afterEach(() => {
......@@ -25,19 +23,14 @@ describe('IdeSidebar', () => {
expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
});
describe('collapsed', () => {
beforeEach((done) => {
vm.$store.state.leftPanelCollapsed = true;
Vue.nextTick(done);
});
it('renders loading icon component', (done) => {
vm.$store.state.loading = true;
it('adds collapsed class', () => {
expect(vm.$el.classList).toContain('is-collapsed');
});
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
it('shows correct icon', () => {
expect(vm.currentIcon).toBe('angle-double-right');
done();
});
});
});
import Vue from 'vue';
import store from 'ee/ide/stores';
import newBranchForm from 'ee/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();
});
});
});
});
......@@ -17,6 +17,9 @@ describe('new dropdown component', () => {
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.path = '';
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [],
};
vm.$mount();
});
......@@ -29,8 +32,12 @@ describe('new dropdown component', () => {
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');
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe(
'Upload file',
);
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
'New directory',
);
});
describe('createNewItem', () => {
......@@ -46,7 +53,7 @@ describe('new dropdown component', () => {
expect(vm.modalType).toBe('tree');
});
it('opens modal when link is clicked', (done) => {
it('opens modal when link is clicked', done => {
vm.$el.querySelectorAll('a')[0].click();
Vue.nextTick(() => {
......@@ -58,12 +65,12 @@ describe('new dropdown component', () => {
});
describe('hideModal', () => {
beforeAll((done) => {
beforeAll(done => {
vm.openModal = true;
Vue.nextTick(done);
});
it('closes modal after toggling', (done) => {
it('closes modal after toggling', done => {
vm.hideModal();
Vue.nextTick()
......
import Vue from 'vue';
import store from 'ee/ide/stores';
import service from 'ee/ide/services';
import modal from 'ee/ide/components/new_dropdown/modal.vue';
import router from 'ee/ide/ide_router';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
import createComponent from 'spec/helpers/vue_mount_component_helper';
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' }],
}),
}));
spyOn(router, 'push');
});
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, {
vm = createComponent(Component, {
type,
branchId: 'master',
path: '',
parent: projectTree,
});
vm.entryName = 'testing';
vm.$mount();
});
it(`sets modal title as ${type}`, () => {
......@@ -93,132 +41,17 @@ describe('new file modal component', () => {
});
describe('createEntryInStore', () => {
it('calls createTempEntry', () => {
spyOn(vm, 'createTempEntry');
it('$emits create', () => {
spyOn(vm, '$emit');
vm.createEntryInStore();
expect(vm.createTempEntry).toHaveBeenCalledWith({
projectId: 'abcproject',
expect(vm.$emit).toHaveBeenCalledWith('create', {
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);
});
}
});
});
});
......@@ -226,12 +59,11 @@ describe('new file modal component', () => {
it('focuses field on mount', () => {
document.body.innerHTML += '<div class="js-test"></div>';
vm = createComponentWithStore(Component, store, {
vm = createComponent(Component, {
type: 'tree',
projectId: 'abcproject',
branchId: 'master',
path: '',
}).$mount('.js-test');
}, '.js-test');
expect(document.activeElement).toBe(vm.$refs.fieldName);
......
import Vue from 'vue';
import upload from 'ee/ide/components/new_dropdown/upload.vue';
import store from 'ee/ide/stores';
import service from 'ee/ide/services';
import router from 'ee/ide/ide_router';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
import createComponent from 'spec/helpers/vue_mount_component_helper';
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' }],
}),
}));
spyOn(router, 'push');
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, {
vm = createComponent(Component, {
branchId: 'master',
path: '',
parent: projectTree,
});
vm.entryName = 'testing';
vm.$mount();
spyOn(vm, '$emit');
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('readFile', () => {
......@@ -108,53 +60,27 @@ describe('new dropdown upload', () => {
name: 'file',
};
it('creates new file', (done) => {
it('creates new file', () => {
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();
expect(vm.$emit).toHaveBeenCalledWith('create', {
name: file.name,
branchId: 'master',
type: 'blob',
content: target.result,
base64: false,
});
});
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) => {
it('splits content on base64 if binary', () => {
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();
expect(vm.$emit).toHaveBeenCalledWith('create', {
name: file.name,
branchId: 'master',
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
base64: true,
});
});
});
......
import Vue from 'vue';
import store from 'ee/ide/stores';
import repoEditButton from 'ee/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();
});
});
});
......@@ -3,6 +3,7 @@ import store from 'ee/ide/stores';
import repoEditor from 'ee/ide/components/repo_editor.vue';
import monacoLoader from 'ee/ide/monaco_loader';
import Editor from 'ee/ide/lib/editor';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
......@@ -12,14 +13,15 @@ describe('RepoEditor', () => {
const f = file();
const RepoEditor = Vue.extend(repoEditor);
vm = new RepoEditor({
store,
vm = createComponentWithStore(RepoEditor, store, {
file: f,
});
f.active = true;
f.tempFile = true;
f.html = 'testing';
vm.$store.state.openFiles.push(f);
vm.$store.getters.activeFile.html = 'testing';
vm.$store.state.entries[f.path] = f;
vm.monaco = true;
vm.$mount();
......@@ -47,9 +49,9 @@ describe('RepoEditor', () => {
describe('when open file is binary and not raw', () => {
beforeEach((done) => {
vm.$store.getters.activeFile.binary = true;
vm.file.binary = true;
Vue.nextTick(done);
vm.$nextTick(done);
});
it('does not render the IDE', () => {
......@@ -97,7 +99,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
expect(vm.editor.createModel).toHaveBeenCalledWith(vm.$store.getters.activeFile);
expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
expect(vm.model).not.toBeNull();
});
......@@ -126,7 +128,7 @@ describe('RepoEditor', () => {
vm.model.setValue('testing 123');
setTimeout(() => {
expect(vm.$store.getters.activeFile.content).toBe('testing 123');
expect(vm.file.content).toBe('testing 123');
done();
});
......
import Vue from 'vue';
import store from 'ee/ide/stores';
import repoFileButtons from 'ee/ide/components/repo_file_buttons.vue';
import { file, resetStore } from '../helpers';
import createVueComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('RepoFileButtons', () => {
const activeFile = file();
......@@ -13,18 +13,14 @@ describe('RepoFileButtons', () => {
activeFile.rawPath = 'test';
activeFile.blamePath = 'test';
activeFile.commitsPath = 'test';
activeFile.active = true;
store.state.openFiles.push(activeFile);
return new RepoFileButtons({
store,
}).$mount();
return createVueComponent(RepoFileButtons, {
file: activeFile,
});
}
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => {
......
import Vue from 'vue';
import store from 'ee/ide/stores';
import repoFile from 'ee/ide/components/repo_file.vue';
import { file, resetStore } from '../helpers';
import router from 'ee/ide/ide_router';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('RepoFile', () => {
const updated = 'updated';
let vm;
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
return new RepoFile({
store,
propsData,
}).$mount();
vm = createComponentWithStore(RepoFile, store, propsData);
vm.$mount();
}
afterEach(() => {
resetStore(vm.$store);
vm.$destroy();
});
it('renders link, icon and name', () => {
const RepoFile = Vue.extend(repoFile);
vm = new RepoFile({
store,
propsData: {
file: file('t4'),
},
createComponent({
file: file('t4'),
level: 0,
});
spyOn(vm, 'timeFormated').and.returnValue(updated);
vm.$mount();
const name = vm.$el.querySelector('.ide-file-name');
......@@ -37,58 +32,19 @@ describe('RepoFile', () => {
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({
it('fires clickFile when the link is clicked', done => {
spyOn(router, 'push');
createComponent({
file: file('t3'),
level: 0,
});
spyOn(vm, 'clickFile');
vm.$el.querySelector('.file-name').click();
expect(vm.clickFile).toHaveBeenCalledWith(vm.file);
});
describe('submodule', () => {
let f;
beforeEach(() => {
f = file('submodule name', '123456789');
f.type = 'submodule';
setTimeout(() => {
expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`);
vm = createComponent({
file: f,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders submodule short ID', () => {
expect(vm.$el.querySelector('.commit-sha').textContent.trim()).toBe('12345678');
done();
});
});
......@@ -104,21 +60,21 @@ describe('RepoFile', () => {
},
};
vm = createComponent({
createComponent({
file: f,
level: 0,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders lock icon', () => {
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => {
expect(vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle).toContain('Locked by testuser');
expect(
vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset
.originalTitle,
).toContain('Locked by testuser');
});
});
});
import Vue from 'vue';
import store from 'ee/ide/stores';
import repoPreview from 'ee/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');
});
});
......@@ -21,6 +21,8 @@ describe('RepoTab', () => {
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
......@@ -57,7 +59,7 @@ describe('RepoTab', () => {
vm.$el.querySelector('.multi-file-tab-close').click();
expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
});
it('changes icon on hover', (done) => {
......@@ -125,7 +127,8 @@ describe('RepoTab', () => {
});
vm.$store.state.openFiles.push(tab);
vm.$store.state.changedFiles.push(tab);
vm.$store.dispatch('setFileActive', tab);
vm.$store.state.entries[tab.path] = tab;
vm.$store.dispatch('setFileActive', tab.path);
vm.$el.querySelector('.multi-file-tab-close').click();
......@@ -144,7 +147,8 @@ describe('RepoTab', () => {
tab,
});
vm.$store.state.openFiles.push(tab);
vm.$store.dispatch('setFileActive', tab);
vm.$store.state.entries[tab.path] = tab;
vm.$store.dispatch('setFileActive', tab.path);
vm.$el.querySelector('.multi-file-tab-close').click();
......
import Vue from 'vue';
import store from 'ee/ide/stores';
import repoTabs from 'ee/ide/components/repo_tabs.vue';
import { file, resetStore } from '../helpers';
import createComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('RepoTabs', () => {
const openedFiles = [file('open1'), file('open2')];
const RepoTabs = Vue.extend(repoTabs);
let vm;
function createComponent(el = null) {
const RepoTabs = Vue.extend(repoTabs);
return new RepoTabs({
store,
}).$mount(el);
}
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders a list of tabs', (done) => {
vm = createComponent();
it('renders a list of tabs', done => {
vm = createComponent(RepoTabs, {
files: openedFiles,
viewer: 'editor',
hasChanges: false,
});
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();
expect(tabs[0].classList.contains('active')).toEqual(true);
expect(tabs[1].classList.contains('active')).toEqual(false);
done();
});
});
describe('updated', () => {
it('sets showShadow as true when scroll width is larger than width', (done) => {
it('sets showShadow as true when scroll width is larger than width', done => {
const el = document.createElement('div');
el.innerHTML = '<div id="test-app"></div>';
document.body.appendChild(el);
......@@ -56,18 +50,26 @@ describe('RepoTabs', () => {
`;
document.head.appendChild(style);
vm = createComponent('#test-app');
openedFiles[0].active = true;
vm.$nextTick()
vm = createComponent(
RepoTabs,
{
files: [],
viewer: 'editor',
hasChanges: false,
},
'#test-app',
);
vm
.$nextTick()
.then(() => {
expect(vm.showShadow).toBeFalsy();
expect(vm.showShadow).toEqual(false);
vm.$store.state.openFiles = openedFiles;
vm.files = openedFiles;
})
.then(vm.$nextTick)
.then(() => {
expect(vm.showShadow).toBeTruthy();
expect(vm.showShadow).toEqual(true);
style.remove();
el.remove();
......
import store from 'ee/ide/stores';
import service from 'ee/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);
});
});
});
This diff is collapsed.
......@@ -26,52 +26,6 @@ describe('Multi-file store getters', () => {
});
});
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());
......
......@@ -197,6 +197,10 @@ describe('IDE commit module actions', () => {
changed: true,
});
store.state.openFiles = store.state.changedFiles;
store.state.changedFiles.forEach((changedFile) => {
store.state.entries[changedFile.path] = changedFile;
});
});
it('updates stores working reference', (done) => {
......@@ -326,6 +330,10 @@ describe('IDE commit module actions', () => {
store.state.changedFiles[0].active = true;
store.state.openFiles = store.state.changedFiles;
store.state.openFiles.forEach((f) => {
store.state.entries[f.path] = f;
});
store.state.commit.commitAction = '2';
store.state.commit.commitMessage = 'testing 123';
});
......
......@@ -9,12 +9,14 @@ describe('Multi-file store file mutations', () => {
beforeEach(() => {
localState = state();
localFile = file();
localState.entries[localFile.path] = localFile;
});
describe('SET_FILE_ACTIVE', () => {
it('sets the file active', () => {
mutations.SET_FILE_ACTIVE(localState, {
file: localFile,
path: localFile.path,
active: true,
});
......@@ -24,7 +26,7 @@ describe('Multi-file store file mutations', () => {
describe('TOGGLE_FILE_OPEN', () => {
beforeEach(() => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
});
it('adds into opened files', () => {
......@@ -33,7 +35,7 @@ describe('Multi-file store file mutations', () => {
});
it('removes from opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
expect(localFile.opened).toBeFalsy();
expect(localState.openFiles.length).toBe(0);
......@@ -81,7 +83,7 @@ describe('Multi-file store file mutations', () => {
it('sets content', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
path: localFile.path,
content: 'test',
});
......@@ -90,7 +92,7 @@ describe('Multi-file store file mutations', () => {
it('sets changed if content does not match raw', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
path: localFile.path,
content: 'testing',
});
......@@ -102,7 +104,7 @@ describe('Multi-file store file mutations', () => {
localFile.tempFile = true;
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
path: localFile.path,
content: '',
});
......@@ -117,32 +119,16 @@ describe('Multi-file store file mutations', () => {
});
it('resets content and changed', () => {
mutations.DISCARD_FILE_CHANGES(localState, localFile);
mutations.DISCARD_FILE_CHANGES(localState, localFile.path);
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);
});
});
describe('ADD_FILE_TO_CHANGED', () => {
it('adds file into changed files array', () => {
const f = file();
mutations.ADD_FILE_TO_CHANGED(localState, f);
mutations.ADD_FILE_TO_CHANGED(localState, localFile.path);
expect(localState.changedFiles.length).toBe(1);
});
......@@ -150,11 +136,9 @@ describe('Multi-file store file mutations', () => {
describe('REMOVE_FILE_FROM_CHANGED', () => {
it('removes files from changed files array', () => {
const f = file();
localState.changedFiles.push(localFile);
localState.changedFiles.push(f);
mutations.REMOVE_FILE_FROM_CHANGED(localState, f);
mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path);
expect(localState.changedFiles.length).toBe(0);
});
......@@ -162,14 +146,12 @@ describe('Multi-file store file mutations', () => {
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
const f = file();
mutations.TOGGLE_FILE_CHANGED(localState, {
file: f,
file: localFile,
changed: true,
});
expect(f.changed).toBeTruthy();
expect(localFile.changed).toBeTruthy();
});
});
});
......@@ -9,15 +9,17 @@ describe('Multi-file store tree mutations', () => {
beforeEach(() => {
localState = state();
localTree = file();
localState.entries[localTree.path] = localTree;
});
describe('TOGGLE_TREE_OPEN', () => {
it('toggles tree open', () => {
mutations.TOGGLE_TREE_OPEN(localState, localTree);
mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
expect(localTree.opened).toBeTruthy();
mutations.TOGGLE_TREE_OPEN(localState, localTree);
mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
expect(localTree.opened).toBeFalsy();
});
......@@ -35,37 +37,21 @@ describe('Multi-file store tree mutations', () => {
}];
it('adds directory data', () => {
localState.trees['project/master'] = {
tree: [],
};
mutations.SET_DIRECTORY_DATA(localState, {
data,
tree: localState,
treePath: 'project/master',
});
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,
});
const tree = localState.trees['project/master'];
expect(localTree.tree.length).toBe(1);
expect(localTree.tree[0].name).toBe(tmpEntry.name);
expect(tree.tree.length).toBe(3);
expect(tree.tree[0].name).toBe('tree');
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
});
});
......
......@@ -9,6 +9,8 @@ describe('Multi-file store mutations', () => {
beforeEach(() => {
localState = state();
entry = file();
localState.entries[entry.path] = entry;
});
describe('SET_INITIAL_DATA', () => {
......@@ -21,34 +23,6 @@ describe('Multi-file store mutations', () => {
});
});
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 });
......@@ -71,18 +45,6 @@ describe('Multi-file store mutations', () => {
});
});
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('SET_LEFT_PANEL_COLLAPSED', () => {
it('sets left panel collapsed', () => {
mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
......
This diff is collapsed.
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