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