Commit b3f74903 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'ph-multi-file-editor-restructure-data' into 'master'

Refactored multi-file data structure

See merge request gitlab-org/gitlab-ce!14862
parents c3d31038 bdbcf58a
...@@ -11,7 +11,9 @@ import Helper from '../helpers/repo_helper'; ...@@ -11,7 +11,9 @@ import Helper from '../helpers/repo_helper';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
export default { export default {
data: () => Store, data() {
return Store;
},
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
RepoSidebar, RepoSidebar,
......
...@@ -9,7 +9,9 @@ import { visitUrl } from '../../lib/utils/url_utility'; ...@@ -9,7 +9,9 @@ import { visitUrl } from '../../lib/utils/url_utility';
export default { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
data: () => Store, data() {
return Store;
},
components: { components: {
PopupDialog, PopupDialog,
......
...@@ -3,7 +3,9 @@ import Store from '../stores/repo_store'; ...@@ -3,7 +3,9 @@ import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
export default { export default {
data: () => Store, data() {
return Store;
},
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
buttonLabel() { buttonLabel() {
......
...@@ -5,7 +5,9 @@ import Service from '../services/repo_service'; ...@@ -5,7 +5,9 @@ import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
const RepoEditor = { const RepoEditor = {
data: () => Store, data() {
return Store;
},
destroyed() { destroyed() {
if (Helper.monacoInstance) { if (Helper.monacoInstance) {
...@@ -93,7 +95,7 @@ const RepoEditor = { ...@@ -93,7 +95,7 @@ const RepoEditor = {
}, },
blobRaw() { blobRaw() {
if (Helper.monacoInstance && !this.isTree) { if (Helper.monacoInstance) {
this.setupEditor(); this.setupEditor();
} }
}, },
......
<script> <script>
import TimeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
const RepoFile = { export default {
mixins: [TimeAgoMixin], mixins: [
props: { repoMixin,
file: { timeAgoMixin,
type: Object, ],
required: true, props: {
file: {
type: Object,
required: true,
},
}, },
isMini: { computed: {
type: Boolean, fileIcon() {
required: false, const classObj = {
default: false, 'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
return classObj;
},
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
};
},
}, },
loading: { methods: {
type: Object, linkClicked(file) {
required: false, eventHub.$emit('fileNameClicked', file);
default() { return { tree: false }; }, },
}, },
hasFiles: { };
type: Boolean,
required: false,
default: false,
},
activeFile: {
type: Object,
required: true,
},
},
computed: {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
fileIcon() {
const classObj = {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
};
return classObj;
},
fileIndentation() {
return {
'margin-left': `${this.file.level * 10}px`,
};
},
activeFileClass() {
return {
active: this.activeFile.url === this.file.url,
};
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
},
},
};
export default RepoFile;
</script> </script>
<template> <template>
<tr <tr
v-if="canShowFile" class="file"
class="file" @click.prevent="linkClicked(file)">
:class="activeFileClass" <td>
@click.prevent="linkClicked(file)"> <i
<td> class="fa fa-fw file-icon"
<i :class="fileIcon"
class="fa fa-fw file-icon" :style="levelIndentation"
:class="fileIcon" aria-hidden="true"
:style="fileIndentation" >
aria-label="file icon"> </i>
</i> <a
<a :href="file.url"
:href="file.url" class="repo-file-name"
class="repo-file-name" >
:title="file.url"> {{ file.name }}
{{file.name}} </a>
</a> </td>
</td>
<template v-if="!isMini"> <template v-if="!isMini">
<td class="hidden-sm hidden-xs"> <td class="hidden-sm hidden-xs">
<div class="commit-message"> <a
<a @click.stop :href="file.lastCommitUrl"> @click.stop
{{file.lastCommitMessage}} :href="file.lastCommit.url"
class="commit-message"
>
{{ file.lastCommit.message }}
</a> </a>
</div> </td>
</td>
<td class="hidden-xs text-right"> <td class="commit-update hidden-xs text-right">
<span <span :title="tooltipTitle(file.lastCommit.updatedAt)">
class="commit-update" {{ timeFormated(file.lastCommit.updatedAt) }}
:title="tooltipTitle(file.lastCommitUpdate)"> </span>
{{timeFormated(file.lastCommitUpdate)}} </td>
</span> </template>
</td> </tr>
</template>
</tr>
</template> </template>
...@@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper'; ...@@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = { const RepoFileButtons = {
data: () => Store, data() {
return Store;
},
mixins: [RepoMixin], mixins: [RepoMixin],
......
<script>
const RepoFileOptions = {
props: {
isMini: {
type: Boolean,
required: false,
default: false,
},
projectName: {
type: String,
required: true,
},
},
};
export default RepoFileOptions;
</script>
<template>
<tr v-if="isMini" class="repo-file-options">
<td>
<span class="title">{{projectName}}</span>
</td>
</tr>
</template>
<script> <script>
const RepoLoadingFile = { import repoMixin from '../mixins/repo_mixin';
props: {
loading: {
type: Object,
required: false,
default: {},
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
},
},
methods: { export default {
lineOfCode(n) { mixins: [
return `skeleton-line-${n}`; repoMixin,
],
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
},
}, },
}, };
};
export default RepoLoadingFile;
</script> </script>
<template> <template>
<tr <tr
v-if="showGhostLines" class="loading-file"
class="loading-file"> aria-label="Loading files"
>
<td> <td>
<div <div
class="animation-container animation-container-small"> class="animation-container animation-container-small">
...@@ -48,29 +28,28 @@ export default RepoLoadingFile; ...@@ -48,29 +28,28 @@ export default RepoLoadingFile;
</div> </div>
</div> </div>
</td> </td>
<template v-if="!isMini">
<td <td
v-if="!isMini" class="hidden-sm hidden-xs">
class="hidden-sm hidden-xs"> <div class="animation-container">
<div class="animation-container"> <div
<div v-for="n in 6"
v-for="n in 6" :key="n"
:key="n" :class="lineOfCode(n)">
:class="lineOfCode(n)"> </div>
</div> </div>
</div> </td>
</td>
<td <td
v-if="!isMini" class="hidden-xs">
class="hidden-xs"> <div class="animation-container animation-container-small animation-container-right">
<div class="animation-container animation-container-small"> <div
<div v-for="n in 6"
v-for="n in 6" :key="n"
:key="n" :class="lineOfCode(n)">
:class="lineOfCode(n)"> </div>
</div> </div>
</div> </td>
</td> </template>
</tr> </tr>
</template> </template>
<script> <script>
import RepoMixin from '../mixins/repo_mixin'; import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
const RepoPreviousDirectory = { export default {
props: { mixins: [
prevUrl: { repoMixin,
type: String, ],
required: true, props: {
prevUrl: {
type: String,
required: true,
},
}, },
}, computed: {
colSpanCondition() {
mixins: [RepoMixin], return this.isMini ? undefined : 3;
},
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
}, },
}, methods: {
linkClicked(file) {
methods: { eventHub.$emit('goToPreviousDirectoryClicked', file);
linkClicked(file) { },
this.$emit('linkclicked', file);
}, },
}, };
};
export default RepoPreviousDirectory;
</script> </script>
<template> <template>
<tr class="prev-directory"> <tr class="file prev-directory">
<td <td
:colspan="colSpanCondition" :colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)"> class="table-cell"
<a :href="prevUrl">..</a> @click.prevent="linkClicked(prevUrl)"
</td> >
</tr> <a :href="prevUrl">...</a>
</td>
</tr>
</template> </template>
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data() {
return Store;
},
computed: { computed: {
html() { html() {
return this.activeFile.html; return this.activeFile.html;
......
<script> <script>
import _ from 'underscore';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFileOptions from './repo_file_options.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
...@@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin'; ...@@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
'repo-file-options': RepoFileOptions,
'repo-previous-directory': RepoPreviousDirectory, 'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile, 'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile, 'repo-loading-file': RepoLoadingFile,
}, },
created() { created() {
window.addEventListener('popstate', this.checkHistory); window.addEventListener('popstate', this.checkHistory);
}, },
destroyed() { destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory); window.removeEventListener('popstate', this.checkHistory);
}, },
mounted() {
eventHub.$on('fileNameClicked', this.fileClicked);
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data() {
return Store;
},
computed: {
flattendFiles() {
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
data: () => Store, return _.chain(this.files)
.map(arr => [arr, mapFiles(arr)])
.flatten()
.value();
},
},
methods: { methods: {
checkHistory() { checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
...@@ -52,21 +67,21 @@ export default { ...@@ -52,21 +67,21 @@ export default {
}, },
fileClicked(clickedFile, lineNumber) { fileClicked(clickedFile, lineNumber) {
let file = clickedFile; const file = clickedFile;
if (file.loading) return; if (file.loading) return;
file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); Helper.setDirectoryToClosed(file);
file.loading = false;
Store.setActiveLine(lineNumber); Store.setActiveLine(lineNumber);
} else { } else {
const openFile = Helper.getFileFromPath(file.url); const openFile = Helper.getFileFromPath(file.url);
if (openFile) { if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile); Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber); Store.setActiveLine(lineNumber);
} else { } else {
file.loading = true;
Service.url = file.url; Service.url = file.url;
Helper.getContent(file) Helper.getContent(file)
.then(() => { .then(() => {
...@@ -81,7 +96,7 @@ export default { ...@@ -81,7 +96,7 @@ export default {
goToPreviousDirectoryClicked(prevURL) { goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL; Service.url = prevURL;
Helper.getContent(null) Helper.getContent(null, true)
.then(() => Helper.scrollTabsRight()) .then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError); .catch(Helper.loadingError);
}, },
...@@ -92,38 +107,43 @@ export default { ...@@ -92,38 +107,43 @@ export default {
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}"> <div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table"> <table class="table">
<thead v-if="!isMini"> <thead>
<tr> <tr>
<th class="name">Name</th> <th
<th class="hidden-sm hidden-xs last-commit">Last commit</th> v-if="isMini"
<th class="hidden-xs last-update text-right">Last update</th> class="repo-file-options title"
>
<strong class="clgray">
{{ projectName }}
</strong>
</th>
<template v-else>
<th class="name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
Last commit
</th>
<th class="hidden-xs last-update text-right">
Last update
</th>
</template>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<repo-file-options
:is-mini="isMini"
:project-name="projectName"
/>
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="!isRoot && !loading.tree"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/> />
<repo-loading-file <repo-loading-file
v-if="!flattendFiles.length && loading.tree"
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
:loading="loading"
:has-files="!!files.length"
:is-mini="isMini"
/> />
<repo-file <repo-file
v-for="file in files" v-for="file in flattendFiles"
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini"
@linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"
/> />
</tbody> </tbody>
</table> </table>
......
...@@ -26,11 +26,13 @@ const RepoTab = { ...@@ -26,11 +26,13 @@ const RepoTab = {
}, },
methods: { methods: {
tabClicked: Store.setActiveFiles, tabClicked(file) {
Store.setActiveFiles(file);
},
closeTab(file) { closeTab(file) {
if (file.changed) return; if (file.changed) return;
this.$emit('tabclosed', file);
Store.removeFromOpenedFiles(file);
}, },
}, },
}; };
...@@ -39,25 +41,28 @@ export default RepoTab; ...@@ -39,25 +41,28 @@ export default RepoTab;
</script> </script>
<template> <template>
<li @click="tabClicked(tab)"> <li
<a :class="{ active : tab.active }"
href="#0" @click="tabClicked(tab)"
class="close" >
@click.stop.prevent="closeTab(tab)" <button
:aria-label="closeLabel"> type="button"
<i class="close-btn"
class="fa" @click.stop.prevent="closeTab(tab)"
:class="changedClass" :aria-label="closeLabel">
aria-hidden="true"> <i
</i> class="fa"
</a> :class="changedClass"
aria-hidden="true">
</i>
</button>
<a <a
href="#" href="#"
class="repo-tab" class="repo-tab"
:title="tab.url" :title="tab.url"
@click.prevent="tabClicked(tab)"> @click.prevent="tabClicked(tab)">
{{tab.name}} {{tab.name}}
</a> </a>
</li> </li>
</template> </template>
<script> <script>
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
const RepoTabs = { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
components: {
components: { 'repo-tab': RepoTab,
'repo-tab': RepoTab,
},
data: () => Store,
methods: {
tabClosed(file) {
Store.removeFromOpenedFiles(file);
}, },
}, data() {
}; return Store;
},
export default RepoTabs; };
</script> </script>
<template> <template>
<ul id="tabs"> <ul
<repo-tab id="tabs"
v-for="tab in openedFiles" class="list-unstyled"
:key="tab.id" >
:tab="tab" <repo-tab
:class="{'active' : tab.active}" v-for="tab in openedFiles"
@tabclosed="tabClosed" :key="tab.id"
/> :tab="tab"
<li class="tabs-divider" /> />
</ul> <li class="tabs-divider" />
</ul>
</template> </template>
import Vue from 'vue';
export default new Vue();
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Flash from '../../flash'; import Flash from '../../flash';
...@@ -25,10 +26,6 @@ const RepoHelper = { ...@@ -25,10 +26,6 @@ const RepoHelper = {
key: '', key: '',
isTree(data) {
return Object.hasOwnProperty.call(data, 'blobs');
},
Time: window.performance Time: window.performance
&& window.performance.now && window.performance.now
? window.performance ? window.performance
...@@ -58,13 +55,20 @@ const RepoHelper = { ...@@ -58,13 +55,20 @@ const RepoHelper = {
}, },
setDirectoryOpen(tree, title) { setDirectoryOpen(tree, title) {
const file = tree; if (!tree) return;
if (!file) return undefined;
Object.assign(tree, {
opened: true,
});
RepoHelper.updateHistoryEntry(tree.url, title);
},
file.opened = true; setDirectoryToClosed(entry) {
file.icon = 'fa-folder-open'; Object.assign(entry, {
RepoHelper.updateHistoryEntry(file.url, title); opened: false,
return file; files: [],
});
}, },
isRenderable() { isRenderable() {
...@@ -81,63 +85,23 @@ const RepoHelper = { ...@@ -81,63 +85,23 @@ const RepoHelper = {
.catch(RepoHelper.loadingError); .catch(RepoHelper.loadingError);
}, },
// when you open a directory you need to put the directory files under getContent(treeOrFile, emptyFiles = false) {
// the directory... This will merge the list of the current directory and the new list. let file = treeOrFile;
getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted;
const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
if (!indexOfFile) return newListSorted;
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
},
// within the get new merged list this does the merging of the current list of files
// and the new list of files. The files are never "in" another directory they just
// appear like they are because of the margin.
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1;
const file = newFile;
file.level = inDirectory.level + 1;
oldList.splice(fileIndex, 0, file);
});
return oldList;
},
compareFilesCaseInsensitive(a, b) {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
if (a.level > 0) return 0;
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
isRoot(url) { if (!Store.files.length) {
// the url we are requesting -> split by the project URL. Grab the right side. Store.loading.tree = true;
const isRoot = !!url.split(Store.projectUrl)[1] }
// remove the first "/"
.slice(1)
// split this by "/"
.split('/')
// remove the first two items of the array... usually /tree/master.
.slice(2)
// we want to know the length of the array.
// If greater than 0 not root.
.length;
return isRoot;
},
getContent(treeOrFile) {
let file = treeOrFile;
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title']; if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
Store.isInitialRoot = Store.isRoot;
}
Store.isTree = RepoHelper.isTree(data); if (file && file.type === 'blob') {
if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
...@@ -145,38 +109,40 @@ const RepoHelper = { ...@@ -145,38 +109,40 @@ const RepoHelper = {
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
} else if (!Store.isPreviewView()) { } else if (!Store.isPreviewView() && !data.render_error) {
if (!data.render_error) { Service.getRaw(data.raw_path)
Service.getRaw(data.raw_path) .then((rawResponse) => {
.then((rawResponse) => { Store.blobRaw = rawResponse.data;
Store.blobRaw = rawResponse.data; data.plain = rawResponse.data;
data.plain = rawResponse.data; RepoHelper.setFile(data, file);
RepoHelper.setFile(data, file); }).catch(RepoHelper.loadingError);
}).catch(RepoHelper.loadingError);
}
} }
if (Store.isPreviewView()) { if (Store.isPreviewView()) {
RepoHelper.setFile(data, file); RepoHelper.setFile(data, file);
} }
} else {
Store.loading.tree = false;
RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
// if the file tree is empty if (emptyFiles) {
if (Store.files.length === 0) { Store.files = [];
const parentURL = Service.blobURLtoParentTree(Service.url);
Service.url = parentURL;
RepoHelper.getContent();
} }
} else {
// it's a tree this.addToDirectory(file, data);
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
const newDirectory = RepoHelper.dataToListOfFiles(data);
Store.addFilesToDirectory(file, Store.files, newDirectory);
Store.prevURL = Service.blobURLtoParentTree(Service.url); Store.prevURL = Service.blobURLtoParentTree(Service.url);
} }
}).catch(RepoHelper.loadingError); }).catch(RepoHelper.loadingError);
}, },
addToDirectory(file, data) {
const tree = file || Store;
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
tree.files = files;
},
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh. newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
...@@ -190,57 +156,39 @@ const RepoHelper = { ...@@ -190,57 +156,39 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
serializeBlob(blob) { serializeRepoEntity(type, entity, level = 0) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
simpleBlob.loading = false;
return simpleBlob;
},
serializeTree(tree) {
return RepoHelper.serializeRepoEntity('tree', tree);
},
serializeSubmodule(submodule) {
return RepoHelper.serializeRepoEntity('submodule', submodule);
},
serializeRepoEntity(type, entity) {
const { url, name, icon, last_commit } = entity; const { url, name, icon, last_commit } = entity;
const returnObj = {
return {
type, type,
name, name,
url, url,
level,
icon: `fa-${icon}`, icon: `fa-${icon}`,
level: 0, files: [],
loading: false, loading: false,
opened: false,
// eslint-disable-next-line camelcase
lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`,
message: last_commit.message,
updatedAt: last_commit.committed_date,
} : {},
}; };
if (entity.last_commit) {
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
} else {
returnObj.lastCommitUrl = '';
}
return returnObj;
}, },
scrollTabsRight() { scrollTabsRight() {
// wait for the transition. 0.1 seconds. const tabs = document.getElementById('tabs');
setTimeout(() => { if (!tabs) return;
const tabs = document.getElementById('tabs'); tabs.scrollLeft = tabs.scrollWidth;
if (!tabs) return;
tabs.scrollLeft = tabs.scrollWidth;
}, 200);
}, },
dataToListOfFiles(data) { dataToListOfFiles(data, level) {
const { blobs, trees, submodules } = data; const { blobs, trees, submodules } = data;
return [ return [
...blobs.map(blob => RepoHelper.serializeBlob(blob)), ...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
...trees.map(tree => RepoHelper.serializeTree(tree)), ...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), ...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
]; ];
}, },
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Service from './services/repo_service'; import Service from './services/repo_service';
import Store from './stores/repo_store'; import Store from './stores/repo_store';
import Repo from './components/repo.vue'; import Repo from './components/repo.vue';
...@@ -33,6 +34,8 @@ function setInitialStore(data) { ...@@ -33,6 +34,8 @@ function setInitialStore(data) {
Store.onTopOfBranch = data.onTopOfBranch; Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl); Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl); Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.isRoot = convertPermissionToBoolean(data.root);
Store.isInitialRoot = convertPermissionToBoolean(data.root);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable(); Store.checkIsCommitable();
Store.setBranchHash(); Store.setBranchHash();
......
...@@ -2,14 +2,13 @@ import Helper from '../helpers/repo_helper'; ...@@ -2,14 +2,13 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
monaco: {},
monacoLoading: false, monacoLoading: false,
service: '', service: '',
canCommit: false, canCommit: false,
onTopOfBranch: false, onTopOfBranch: false,
editMode: false, editMode: false,
isTree: false, isRoot: null,
isRoot: false, isInitialRoot: null,
prevURL: '', prevURL: '',
projectId: '', projectId: '',
projectName: '', projectName: '',
...@@ -39,23 +38,11 @@ const RepoStore = { ...@@ -39,23 +38,11 @@ const RepoStore = {
newMrTemplateUrl: '', newMrTemplateUrl: '',
branchChanged: false, branchChanged: false,
commitMessage: '', commitMessage: '',
binaryTypes: {
png: false,
md: false,
svg: false,
unknown: false,
},
loading: { loading: {
tree: false, tree: false,
blob: false, blob: false,
}, },
resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => {
RepoStore.binaryTypes[key] = false;
});
},
setBranchHash() { setBranchHash() {
return Service.getBranch() return Service.getBranch()
.then((data) => { .then((data) => {
...@@ -72,10 +59,6 @@ const RepoStore = { ...@@ -72,10 +59,6 @@ const RepoStore = {
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
}, },
addFilesToDirectory(inDirectory, currentList, newList) {
RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
},
toggleRawPreview() { toggleRawPreview() {
RepoStore.activeFile.raw = !RepoStore.activeFile.raw; RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source'; RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
...@@ -129,30 +112,6 @@ const RepoStore = { ...@@ -129,30 +112,6 @@ const RepoStore = {
RepoStore.activeFileLabel = 'Display source'; RepoStore.activeFileLabel = 'Display source';
}, },
removeChildFilesOfTree(tree) {
let foundTree = false;
const treeToClose = tree;
let canStopSearching = false;
RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
canStopSearching = true;
return true;
}
if (canStopSearching) return true;
if (isItTheTreeWeWant) foundTree = true;
if (foundTree) return file.level <= treeToClose.level;
return true;
});
treeToClose.opened = false;
treeToClose.icon = 'fa-folder';
return treeToClose;
},
removeFromOpenedFiles(file) { removeFromOpenedFiles(file) {
if (file.type === 'tree') return; if (file.type === 'tree') return;
let foundIndex; let foundIndex;
...@@ -186,6 +145,7 @@ const RepoStore = { ...@@ -186,6 +145,7 @@ const RepoStore = {
if (openedFilesAlreadyExists) return; if (openedFilesAlreadyExists) return;
openFile.changed = false; openFile.changed = false;
openFile.active = true;
RepoStore.openedFiles.push(openFile); RepoStore.openedFiles.push(openFile);
}, },
......
...@@ -198,6 +198,13 @@ a { ...@@ -198,6 +198,13 @@ a {
height: 12px; height: 12px;
} }
&.animation-container-right {
.skeleton-line-2 {
left: 0;
right: 150px;
}
}
&::before { &::before {
animation-duration: 1s; animation-duration: 1s;
animation-fill-mode: forwards; animation-fill-mode: forwards;
......
...@@ -153,28 +153,13 @@ ...@@ -153,28 +153,13 @@
overflow-x: auto; overflow-x: auto;
li { li {
animation: swipeRightAppear ease-in 0.1s; position: relative;
animation-iteration-count: 1;
transform-origin: 0% 50%;
list-style-type: none;
background: $gray-normal; background: $gray-normal;
display: inline-block;
padding: #{$gl-padding / 2} $gl-padding; padding: #{$gl-padding / 2} $gl-padding;
border-right: 1px solid $white-dark; border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
white-space: nowrap;
cursor: pointer; cursor: pointer;
&.remove {
animation: swipeRightDissapear ease-in 0.1s;
animation-iteration-count: 1;
transform-origin: 0% 50%;
a {
width: 0;
}
}
&.active { &.active {
background: $white-light; background: $white-light;
border-bottom: none; border-bottom: none;
...@@ -182,17 +167,21 @@ ...@@ -182,17 +167,21 @@
a { a {
@include str-truncated(100px); @include str-truncated(100px);
color: $black; color: $gl-text-color;
vertical-align: middle; vertical-align: middle;
text-decoration: none; text-decoration: none;
margin-right: 12px; margin-right: 12px;
}
&.close { .close-btn {
width: auto; position: absolute;
font-size: 15px; right: 8px;
opacity: 1; top: 50%;
margin-right: -6px; padding: 0;
} background: none;
border: 0;
font-size: $gl-font-size;
transform: translateY(-50%);
} }
.close-icon:hover { .close-icon:hover {
...@@ -201,9 +190,6 @@ ...@@ -201,9 +190,6 @@
.close-icon, .close-icon,
.unsaved-icon { .unsaved-icon {
float: right;
margin-top: 3px;
margin-left: 15px;
color: $gray-darkest; color: $gray-darkest;
} }
...@@ -222,9 +208,7 @@ ...@@ -222,9 +208,7 @@
#repo-file-buttons { #repo-file-buttons {
background-color: $white-light; background-color: $white-light;
border-bottom: 1px solid $white-normal;
padding: 5px 10px; padding: 5px 10px;
position: relative;
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
} }
...@@ -287,37 +271,23 @@ ...@@ -287,37 +271,23 @@
overflow: auto; overflow: auto;
} }
table { .table {
margin-bottom: 0; margin-bottom: 0;
} }
tr { tr {
animation: fadein 0.5s; .repo-file-options {
cursor: pointer; padding: 2px 16px;
&.repo-file-options td {
padding: 0;
border-top: none;
background: $gray-light;
width: 100%; width: 100%;
display: inline-block; }
&:first-child {
border-top-left-radius: 2px;
}
.title { .title {
display: inline-block; font-size: 10px;
font-size: 10px; text-transform: uppercase;
text-transform: uppercase; white-space: nowrap;
font-weight: $gl-font-weight-bold; overflow: hidden;
color: $gray-darkest; text-overflow: ellipsis;
white-space: nowrap; vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
padding: 2px 16px;
}
} }
.file-icon { .file-icon {
...@@ -329,11 +299,13 @@ ...@@ -329,11 +299,13 @@
} }
} }
.file {
cursor: pointer;
}
a { a {
@include str-truncated(250px); @include str-truncated(250px);
color: $almost-black; color: $almost-black;
display: inline-block;
vertical-align: middle;
} }
} }
} }
......
...@@ -36,6 +36,7 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -36,6 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
format.json do format.json do
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
response.header['is-root'] = @path.empty?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
......
#repo{ data: { url: content_url, #repo{ data: { root: @path.empty?.to_s,
url: content_url,
project_name: project.name, project_name: project.name,
refs_url: refs_project_path(project, format: :json), refs_url: refs_project_path(project, format: :json),
project_url: project_path(project), project_url: project_path(project),
......
...@@ -134,6 +134,7 @@ describe('RepoCommitSection', () => { ...@@ -134,6 +134,7 @@ describe('RepoCommitSection', () => {
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
el.remove(); el.remove();
RepoStore.openedFiles = [];
}); });
it('shows commit message', () => { it('shows commit message', () => {
......
...@@ -9,6 +9,10 @@ describe('RepoEditButton', () => { ...@@ -9,6 +9,10 @@ describe('RepoEditButton', () => {
return new RepoEditButton().$mount(); return new RepoEditButton().$mount();
} }
afterEach(() => {
RepoStore.openedFiles = [];
});
it('renders an edit button that toggles the view state', (done) => { it('renders an edit button that toggles the view state', (done) => {
RepoStore.isCommitable = true; RepoStore.isCommitable = true;
RepoStore.changedFiles = []; RepoStore.changedFiles = [];
...@@ -38,12 +42,4 @@ describe('RepoEditButton', () => { ...@@ -38,12 +42,4 @@ describe('RepoEditButton', () => {
expect(vm.$el.innerHTML).toBeUndefined(); expect(vm.$el.innerHTML).toBeUndefined();
}); });
describe('methods', () => {
describe('editCancelClicked', () => {
it('sets dialog to open when there are changedFiles');
it('toggles editMode and calls toggleBlobView');
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import RepoStore from '~/repo/stores/repo_store';
import repoEditor from '~/repo/components/repo_editor.vue'; import repoEditor from '~/repo/components/repo_editor.vue';
describe('RepoEditor', () => { describe('RepoEditor', () => {
...@@ -8,6 +9,10 @@ describe('RepoEditor', () => { ...@@ -8,6 +9,10 @@ describe('RepoEditor', () => {
this.vm = new RepoEditor().$mount(); this.vm = new RepoEditor().$mount();
}); });
afterEach(() => {
RepoStore.openedFiles = [];
});
it('renders an ide container', (done) => { it('renders an ide container', (done) => {
this.vm.openedFiles = ['idiidid']; this.vm.openedFiles = ['idiidid'];
this.vm.binary = false; this.vm.binary = false;
......
...@@ -9,6 +9,10 @@ describe('RepoFileButtons', () => { ...@@ -9,6 +9,10 @@ describe('RepoFileButtons', () => {
return new RepoFileButtons().$mount(); return new RepoFileButtons().$mount();
} }
afterEach(() => {
RepoStore.openedFiles = [];
});
it('renders Raw, Blame, History, Permalink and Preview toggle', () => { it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
const activeFile = { const activeFile = {
extension: 'md', extension: 'md',
......
import Vue from 'vue';
import repoFileOptions from '~/repo/components/repo_file_options.vue';
describe('RepoFileOptions', () => {
const projectName = 'projectName';
function createComponent(propsData) {
const RepoFileOptions = Vue.extend(repoFileOptions);
return new RepoFileOptions({
propsData,
}).$mount();
}
it('renders the title and new file/folder buttons if isMini is true', () => {
const vm = createComponent({
isMini: true,
projectName,
});
expect(vm.$el.classList.contains('repo-file-options')).toBeTruthy();
expect(vm.$el.querySelector('.title').textContent).toEqual(projectName);
});
it('does not render if isMini is false', () => {
const vm = createComponent({
isMini: false,
projectName,
});
expect(vm.$el.innerHTML).toBeFalsy();
});
});
import Vue from 'vue'; import Vue from 'vue';
import repoFile from '~/repo/components/repo_file.vue'; import repoFile from '~/repo/components/repo_file.vue';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
import eventHub from '~/repo/event_hub';
import { file } from '../mock_data';
describe('RepoFile', () => { describe('RepoFile', () => {
const updated = 'updated'; const updated = 'updated';
const file = {
icon: 'icon',
url: 'url',
name: 'name',
lastCommitMessage: 'message',
lastCommitUpdate: Date.now(),
level: 10,
};
const activeFile = {
pageTitle: 'pageTitle',
url: 'url',
};
const otherFile = { const otherFile = {
html: '<p class="file-content">html</p>', html: '<p class="file-content">html</p>',
pageTitle: 'otherpageTitle', pageTitle: 'otherpageTitle',
...@@ -29,12 +19,15 @@ describe('RepoFile', () => { ...@@ -29,12 +19,15 @@ describe('RepoFile', () => {
}).$mount(); }).$mount();
} }
beforeEach(() => {
RepoStore.openedFiles = [];
});
it('renders link, icon, name and last commit details', () => { it('renders link, icon, name and last commit details', () => {
const RepoFile = Vue.extend(repoFile); const RepoFile = Vue.extend(repoFile);
const vm = new RepoFile({ const vm = new RepoFile({
propsData: { propsData: {
file, file: file(),
activeFile,
}, },
}); });
spyOn(vm, 'timeFormated').and.returnValue(updated); spyOn(vm, 'timeFormated').and.returnValue(updated);
...@@ -43,28 +36,20 @@ describe('RepoFile', () => { ...@@ -43,28 +36,20 @@ describe('RepoFile', () => {
const name = vm.$el.querySelector('.repo-file-name'); const name = vm.$el.querySelector('.repo-file-name');
const fileIcon = vm.$el.querySelector('.file-icon'); const fileIcon = vm.$el.querySelector('.file-icon');
expect(vm.$el.classList.contains('active')).toBeTruthy(); expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px');
expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px'); expect(name.href).toMatch(`/${vm.file.url}`);
expect(name.title).toEqual(file.url); expect(name.textContent.trim()).toEqual(vm.file.name);
expect(name.href).toMatch(`/${file.url}`); expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(vm.file.lastCommit.message);
expect(name.textContent.trim()).toEqual(file.name);
expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage);
expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated); expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated);
expect(fileIcon.classList.contains(file.icon)).toBeTruthy(); expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy();
expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`); expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`);
}); });
it('does render if hasFiles is true and is loading tree', () => { it('does render if hasFiles is true and is loading tree', () => {
const vm = createComponent({ const vm = createComponent({
file, file: file(),
activeFile,
loading: {
tree: true,
},
hasFiles: true,
}); });
expect(vm.$el.innerHTML).toBeTruthy();
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
}); });
...@@ -75,75 +60,51 @@ describe('RepoFile', () => { ...@@ -75,75 +60,51 @@ describe('RepoFile', () => {
}); });
it('renders a spinner if the file is loading', () => { it('renders a spinner if the file is loading', () => {
file.loading = true; const f = file();
const vm = createComponent({ f.loading = true;
file,
activeFile,
loading: {
tree: true,
},
hasFiles: true,
});
expect(vm.$el.innerHTML).toBeTruthy();
expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${file.level * 10}px`);
});
it('does not render if loading tree', () => {
const vm = createComponent({ const vm = createComponent({
file, file: f,
activeFile,
loading: {
tree: true,
},
}); });
expect(vm.$el.innerHTML).toBeFalsy(); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).not.toBeNull();
expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`);
}); });
it('does not render commit message and datetime if mini', () => { it('does not render commit message and datetime if mini', () => {
RepoStore.openedFiles.push(file());
const vm = createComponent({ const vm = createComponent({
file, file: file(),
activeFile,
isMini: true,
}); });
expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
}); });
it('does not set active class if file is active file', () => {
const vm = createComponent({
file,
activeFile: {},
});
expect(vm.$el.classList.contains('active')).toBeFalsy();
});
it('fires linkClicked when the link is clicked', () => { it('fires linkClicked when the link is clicked', () => {
const vm = createComponent({ const vm = createComponent({
file, file: file(),
activeFile,
}); });
spyOn(vm, 'linkClicked'); spyOn(vm, 'linkClicked');
vm.$el.querySelector('.repo-file-name').click(); vm.$el.click();
expect(vm.linkClicked).toHaveBeenCalledWith(file); expect(vm.linkClicked).toHaveBeenCalledWith(vm.file);
}); });
describe('methods', () => { describe('methods', () => {
describe('linkClicked', () => { describe('linkClicked', () => {
const vm = jasmine.createSpyObj('vm', ['$emit']); it('$emits fileNameClicked with file obj', () => {
spyOn(eventHub, '$emit');
it('$emits linkclicked with file obj', () => { const vm = createComponent({
const theFile = {}; file: file(),
});
repoFile.methods.linkClicked.call(vm, theFile); vm.linkClicked(vm.file);
expect(vm.$emit).toHaveBeenCalledWith('linkclicked', theFile); expect(eventHub.$emit).toHaveBeenCalledWith('fileNameClicked', vm.file);
}); });
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import RepoStore from '~/repo/stores/repo_store';
import repoLoadingFile from '~/repo/components/repo_loading_file.vue'; import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
describe('RepoLoadingFile', () => { describe('RepoLoadingFile', () => {
...@@ -28,6 +29,10 @@ describe('RepoLoadingFile', () => { ...@@ -28,6 +29,10 @@ describe('RepoLoadingFile', () => {
}); });
} }
afterEach(() => {
RepoStore.openedFiles = [];
});
it('renders 3 columns of animated LoC', () => { it('renders 3 columns of animated LoC', () => {
const vm = createComponent({ const vm = createComponent({
loading: { loading: {
...@@ -42,38 +47,16 @@ describe('RepoLoadingFile', () => { ...@@ -42,38 +47,16 @@ describe('RepoLoadingFile', () => {
}); });
it('renders 1 column of animated LoC if isMini', () => { it('renders 1 column of animated LoC if isMini', () => {
RepoStore.openedFiles = new Array(1);
const vm = createComponent({ const vm = createComponent({
loading: { loading: {
tree: true, tree: true,
}, },
hasFiles: false, hasFiles: false,
isMini: true,
}); });
const columns = [...vm.$el.querySelectorAll('td')]; const columns = [...vm.$el.querySelectorAll('td')];
expect(columns.length).toEqual(1); expect(columns.length).toEqual(1);
assertColumns(columns); assertColumns(columns);
}); });
it('does not render if tree is not loading', () => {
const vm = createComponent({
loading: {
tree: false,
},
hasFiles: false,
});
expect(vm.$el.innerHTML).toBeFalsy();
});
it('does not render if hasFiles is true', () => {
const vm = createComponent({
loading: {
tree: true,
},
hasFiles: true,
});
expect(vm.$el.innerHTML).toBeFalsy();
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue'; import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
import eventHub from '~/repo/event_hub';
describe('RepoPrevDirectory', () => { describe('RepoPrevDirectory', () => {
function createComponent(propsData) { function createComponent(propsData) {
...@@ -20,7 +21,7 @@ describe('RepoPrevDirectory', () => { ...@@ -20,7 +21,7 @@ describe('RepoPrevDirectory', () => {
spyOn(vm, 'linkClicked'); spyOn(vm, 'linkClicked');
expect(link.href).toMatch(`/${prevUrl}`); expect(link.href).toMatch(`/${prevUrl}`);
expect(link.textContent).toEqual('..'); expect(link.textContent).toEqual('...');
link.click(); link.click();
...@@ -29,14 +30,17 @@ describe('RepoPrevDirectory', () => { ...@@ -29,14 +30,17 @@ describe('RepoPrevDirectory', () => {
describe('methods', () => { describe('methods', () => {
describe('linkClicked', () => { describe('linkClicked', () => {
const vm = jasmine.createSpyObj('vm', ['$emit']); it('$emits linkclicked with prevUrl', () => {
const prevUrl = 'prevUrl';
const vm = createComponent({
prevUrl,
});
it('$emits linkclicked with file obj', () => { spyOn(eventHub, '$emit');
const file = {};
repoPrevDirectory.methods.linkClicked.call(vm, file); vm.linkClicked(prevUrl);
expect(vm.$emit).toHaveBeenCalledWith('linkclicked', file); expect(eventHub.$emit).toHaveBeenCalledWith('goToPreviousDirectoryClicked', prevUrl);
}); });
}); });
}); });
......
...@@ -3,6 +3,7 @@ import Helper from '~/repo/helpers/repo_helper'; ...@@ -3,6 +3,7 @@ import Helper from '~/repo/helpers/repo_helper';
import RepoService from '~/repo/services/repo_service'; import RepoService from '~/repo/services/repo_service';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
import repoSidebar from '~/repo/components/repo_sidebar.vue'; import repoSidebar from '~/repo/components/repo_sidebar.vue';
import { file } from '../mock_data';
describe('RepoSidebar', () => { describe('RepoSidebar', () => {
let vm; let vm;
...@@ -15,14 +16,15 @@ describe('RepoSidebar', () => { ...@@ -15,14 +16,15 @@ describe('RepoSidebar', () => {
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
RepoStore.files = [];
RepoStore.openedFiles = [];
}); });
it('renders a sidebar', () => { it('renders a sidebar', () => {
RepoStore.files = [{ RepoStore.files = [file()];
id: 0,
}];
RepoStore.openedFiles = []; RepoStore.openedFiles = [];
RepoStore.isRoot = false; RepoStore.isRoot = true;
vm = createComponent(); vm = createComponent();
const thead = vm.$el.querySelector('thead'); const thead = vm.$el.querySelector('thead');
...@@ -30,9 +32,9 @@ describe('RepoSidebar', () => { ...@@ -30,9 +32,9 @@ describe('RepoSidebar', () => {
expect(vm.$el.id).toEqual('sidebar'); expect(vm.$el.id).toEqual('sidebar');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(thead.querySelector('.name').textContent).toEqual('Name'); expect(thead.querySelector('.name').textContent.trim()).toEqual('Name');
expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit'); expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit');
expect(thead.querySelector('.last-update').textContent).toEqual('Last update'); expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy(); expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy(); expect(tbody.querySelector('.loading-file')).toBeFalsy();
...@@ -46,76 +48,74 @@ describe('RepoSidebar', () => { ...@@ -46,76 +48,74 @@ describe('RepoSidebar', () => {
vm = createComponent(); vm = createComponent();
expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
expect(vm.$el.querySelector('thead')).toBeFalsy(); expect(vm.$el.querySelector('thead')).toBeTruthy();
expect(vm.$el.querySelector('tbody .repo-file-options')).toBeTruthy(); expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy();
}); });
it('renders 5 loading files if tree is loading and not hasFiles', () => { it('renders 5 loading files if tree is loading and not hasFiles', () => {
RepoStore.loading = { RepoStore.loading.tree = true;
tree: true,
};
RepoStore.files = []; RepoStore.files = [];
vm = createComponent(); vm = createComponent();
expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
}); });
it('renders a prev directory if isRoot', () => { it('renders a prev directory if is not root', () => {
RepoStore.files = [{ RepoStore.files = [file()];
id: 0, RepoStore.isRoot = false;
}]; RepoStore.loading.tree = false;
RepoStore.isRoot = true;
vm = createComponent(); vm = createComponent();
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
}); });
describe('flattendFiles', () => {
it('returns a flattend array of files', () => {
const f = file();
f.files.push(file('testing 123'));
const files = [f, file()];
vm = createComponent();
vm.files = files;
expect(vm.flattendFiles.length).toBe(3);
expect(vm.flattendFiles[1].name).toBe('testing 123');
});
});
describe('methods', () => { describe('methods', () => {
describe('fileClicked', () => { describe('fileClicked', () => {
it('should fetch data for new file', () => { it('should fetch data for new file', () => {
spyOn(Helper, 'getContent').and.callThrough(); spyOn(Helper, 'getContent').and.callThrough();
const file1 = { RepoStore.files = [file()];
id: 0,
url: '',
};
RepoStore.files = [file1];
RepoStore.isRoot = true; RepoStore.isRoot = true;
vm = createComponent(); vm = createComponent();
vm.fileClicked(file1); vm.fileClicked(RepoStore.files[0]);
expect(Helper.getContent).toHaveBeenCalledWith(file1); expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[0]);
}); });
it('should not fetch data for already opened files', () => { it('should not fetch data for already opened files', () => {
const file = { const f = file();
id: 42, spyOn(Helper, 'getFileFromPath').and.returnValue(f);
url: 'foo',
};
spyOn(Helper, 'getFileFromPath').and.returnValue(file);
spyOn(RepoStore, 'setActiveFiles'); spyOn(RepoStore, 'setActiveFiles');
vm = createComponent(); vm = createComponent();
vm.fileClicked(file); vm.fileClicked(f);
expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file); expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(f);
}); });
it('should hide files in directory if already open', () => { it('should hide files in directory if already open', () => {
spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); spyOn(Helper, 'setDirectoryToClosed').and.callThrough();
const file1 = { const f = file();
id: 0, f.opened = true;
type: 'tree', f.type = 'tree';
url: '', RepoStore.files = [f];
opened: true,
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
vm = createComponent(); vm = createComponent();
vm.fileClicked(file1); vm.fileClicked(RepoStore.files[0]);
expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1); expect(Helper.setDirectoryToClosed).toHaveBeenCalledWith(RepoStore.files[0]);
}); });
}); });
...@@ -131,36 +131,31 @@ describe('RepoSidebar', () => { ...@@ -131,36 +131,31 @@ describe('RepoSidebar', () => {
}); });
describe('back button', () => { describe('back button', () => {
const file1 = { beforeEach(() => {
id: 1, const f = file();
url: 'file1', const file2 = Object.assign({}, file());
}; file2.url = 'test';
const file2 = { RepoStore.files = [f, file2];
id: 2, RepoStore.openedFiles = [];
url: 'file2', RepoStore.isRoot = true;
};
RepoStore.files = [file1, file2];
RepoStore.openedFiles = [file1, file2];
RepoStore.isRoot = true;
vm = createComponent(); vm = createComponent();
vm.fileClicked(file1); });
it('render previous file when using back button', () => { it('render previous file when using back button', () => {
spyOn(Helper, 'getContent').and.callThrough(); spyOn(Helper, 'getContent').and.callThrough();
vm.fileClicked(file2); vm.fileClicked(RepoStore.files[1]);
expect(Helper.getContent).toHaveBeenCalledWith(file2); expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[1]);
Helper.getContent.calls.reset();
history.pushState({ history.pushState({
key: Math.random(), key: Math.random(),
}, '', file1.url); }, '', RepoStore.files[1].url);
const popEvent = document.createEvent('Event'); const popEvent = document.createEvent('Event');
popEvent.initEvent('popstate', true, true); popEvent.initEvent('popstate', true, true);
window.dispatchEvent(popEvent); window.dispatchEvent(popEvent);
expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url); expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(RepoStore.files[1].url);
window.history.pushState({}, null, '/'); window.history.pushState({}, null, '/');
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import repoTab from '~/repo/components/repo_tab.vue'; import repoTab from '~/repo/components/repo_tab.vue';
import RepoStore from '~/repo/stores/repo_store';
describe('RepoTab', () => { describe('RepoTab', () => {
function createComponent(propsData) { function createComponent(propsData) {
...@@ -18,7 +19,7 @@ describe('RepoTab', () => { ...@@ -18,7 +19,7 @@ describe('RepoTab', () => {
const vm = createComponent({ const vm = createComponent({
tab, tab,
}); });
const close = vm.$el.querySelector('.close'); const close = vm.$el.querySelector('.close-btn');
const name = vm.$el.querySelector(`a[title="${tab.url}"]`); const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
spyOn(vm, 'closeTab'); spyOn(vm, 'closeTab');
...@@ -44,26 +45,43 @@ describe('RepoTab', () => { ...@@ -44,26 +45,43 @@ describe('RepoTab', () => {
tab, tab,
}); });
expect(vm.$el.querySelector('.close .fa-circle')).toBeTruthy(); expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy();
}); });
describe('methods', () => { describe('methods', () => {
describe('closeTab', () => { describe('closeTab', () => {
const vm = jasmine.createSpyObj('vm', ['$emit']);
it('returns undefined and does not $emit if file is changed', () => { it('returns undefined and does not $emit if file is changed', () => {
const file = { changed: true }; const tab = {
const returnVal = repoTab.methods.closeTab.call(vm, file); url: 'url',
name: 'name',
changed: true,
};
const vm = createComponent({
tab,
});
spyOn(RepoStore, 'removeFromOpenedFiles');
vm.$el.querySelector('.close-btn').click();
expect(returnVal).toBeUndefined(); expect(RepoStore.removeFromOpenedFiles).not.toHaveBeenCalled();
expect(vm.$emit).not.toHaveBeenCalled();
}); });
it('$emits tabclosed event with file obj', () => { it('$emits tabclosed event with file obj', () => {
const file = { changed: false }; const tab = {
repoTab.methods.closeTab.call(vm, file); url: 'url',
name: 'name',
changed: false,
};
const vm = createComponent({
tab,
});
spyOn(RepoStore, 'removeFromOpenedFiles');
vm.$el.querySelector('.close-btn').click();
expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file); expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(tab);
}); });
}); });
}); });
......
...@@ -16,6 +16,10 @@ describe('RepoTabs', () => { ...@@ -16,6 +16,10 @@ describe('RepoTabs', () => {
return new RepoTabs().$mount(); return new RepoTabs().$mount();
} }
afterEach(() => {
RepoStore.openedFiles = [];
});
it('renders a list of tabs', () => { it('renders a list of tabs', () => {
RepoStore.openedFiles = openedFiles; RepoStore.openedFiles = openedFiles;
...@@ -28,18 +32,4 @@ describe('RepoTabs', () => { ...@@ -28,18 +32,4 @@ describe('RepoTabs', () => {
expect(tabs[1].classList.contains('active')).toBeFalsy(); expect(tabs[1].classList.contains('active')).toBeFalsy();
expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
}); });
describe('methods', () => {
describe('tabClosed', () => {
it('calls removeFromOpenedFiles with file obj', () => {
const file = {};
spyOn(RepoStore, 'removeFromOpenedFiles');
repoTabs.methods.tabClosed(file);
expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file);
});
});
});
}); });
import RepoHelper from '~/repo/helpers/repo_helper';
// eslint-disable-next-line import/prefer-default-export
export const file = (name = 'name') => RepoHelper.serializeRepoEntity('blob', {
icon: 'icon',
url: 'url',
name,
last_commit: {
id: '123',
message: 'test',
committed_date: '',
},
});
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