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