Commit b1b91aa0 authored by Phil Hughes's avatar Phil Hughes

Refactored multi-file data structure

This moves away from storing in a single array just to render the table.
It now stores in a multi-dimensional array/object type where each entry
in the array can have its own tree. This makes storing the data for
future feature a little easier as there is only one way to store the
data.

Previously to insert a directory the code had to insert the directory
& then the file at the right point in the array. Now the directory
can be inserted anywhere & then a file can be quickly added into this
directory.

The rendering is still done with a single array, but this is handled
through underscore. Underscore takes the array & then goes through
each item to flatten it into one. It is done this way to save changing
the markup away from table, keeping it as a table keeps it semantically
correct.
parent 3a7623fc
...@@ -92,7 +92,7 @@ const RepoEditor = { ...@@ -92,7 +92,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: [
repoMixin,
timeAgoMixin,
],
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: true,
}, },
isMini: {
type: Boolean,
required: false,
default: false,
}, },
loading: {
type: Object,
required: false,
default() { return { tree: false }; },
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
activeFile: {
type: Object,
required: true,
},
},
computed: { computed: {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
fileIcon() { fileIcon() {
const classObj = { const classObj = {
'fa-spinner fa-spin': this.file.loading, 'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading, [this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
}; };
return classObj; return classObj;
}, },
levelIndentation() {
fileIndentation() {
return {
'margin-left': `${this.file.level * 10}px`,
};
},
activeFileClass() {
return { return {
active: this.activeFile.url === this.file.url, marginLeft: `${this.file.level * 16}px`,
}; };
}, },
}, },
methods: { methods: {
linkClicked(file) { linkClicked(file) {
this.$emit('linkclicked', file); eventHub.$emit('linkclicked', file);
}, },
}, },
}; };
export default RepoFile;
</script> </script>
<template> <template>
<tr <tr
v-if="canShowFile"
class="file" class="file"
:class="activeFileClass" @click.stop="linkClicked(file)">
@click.prevent="linkClicked(file)">
<td> <td>
<i <i
class="fa fa-fw file-icon" class="fa fa-fw file-icon"
:class="fileIcon" :class="fileIcon"
:style="fileIndentation" :style="levelIndentation"
aria-label="file icon"> aria-hidden="true"
>
</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"
>
{{ file.lastCommit.message }}
</a> </a>
</div>
</td> </td>
<td class="hidden-xs text-right"> <td class="hidden-xs text-right">
<span <span :title="tooltipTitle(file.lastCommit.updatedAt)">
class="commit-update" {{ timeFormated(file.lastCommit.updatedAt) }}
:title="tooltipTitle(file.lastCommitUpdate)">
{{timeFormated(file.lastCommitUpdate)}}
</span> </span>
</td> </td>
</template> </template>
</tr> </tr>
</template> </template>
<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;
},
},
export default {
mixins: [
repoMixin,
],
methods: { methods: {
lineOfCode(n) { lineOfCode(n) {
return `skeleton-line-${n}`; return `skeleton-line-${n}`;
}, },
}, },
}; };
export default RepoLoadingFile;
</script> </script>
<template> <template>
<tr <tr
v-if="showGhostLines"
class="loading-file"> class="loading-file">
<td> <td>
<div <div
...@@ -64,7 +42,7 @@ export default RepoLoadingFile; ...@@ -64,7 +42,7 @@ export default RepoLoadingFile;
<td <td
v-if="!isMini" v-if="!isMini"
class="hidden-xs"> class="hidden-xs">
<div class="animation-container animation-container-small"> <div class="animation-container animation-container-small animation-container-right">
<div <div
v-for="n in 6" v-for="n in 6"
:key="n" :key="n"
......
<script> <script>
import RepoMixin from '../mixins/repo_mixin'; import eventHub from '../event_hub';
const RepoPreviousDirectory = { export default {
props: { props: {
prevUrl: { prevUrl: {
type: String, type: String,
required: true, required: true,
}, },
}, },
mixins: [RepoMixin],
computed: { computed: {
colSpanCondition() { colSpanCondition() {
return this.isMini ? undefined : 3; return this.isMini ? undefined : 3;
}, },
}, },
methods: { methods: {
linkClicked(file) { linkClicked(file) {
this.$emit('linkclicked', file); eventHub.$emit('goToPreviousDirectoryClicked', file);
}, },
}, },
}; };
export default RepoPreviousDirectory;
</script> </script>
<template> <template>
<tr class="prev-directory"> <tr class="prev-directory">
<td <td
:colspan="colSpanCondition" :colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)"> class="table-cell"
<a :href="prevUrl">..</a> @click.prevent="linkClicked(prevUrl)"
>
<a :href="prevUrl">...</a>
</td> </td>
</tr> </tr>
</template> </template>
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
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 +11,39 @@ import RepoMixin from '../mixins/repo_mixin'; ...@@ -11,21 +11,39 @@ 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('linkclicked', this.fileClicked);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory); window.removeEventListener('popstate', this.checkHistory);
}, },
mounted() {
eventHub.$on('linkclicked', this.fileClicked);
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data: () => Store, data: () => Store,
computed: {
flattendFiles() {
const map = (arr) => {
if (arr && arr.tree.length === 0) {
return [];
}
return _.map(arr.tree, a => [a, map(a)]);
};
return _.chain(this.files)
.map(arr => [arr, map(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,16 +70,17 @@ export default { ...@@ -52,16 +70,17 @@ 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);
file.loading = true;
if (openFile) { if (openFile) {
file.loading = false; file.loading = false;
Store.setActiveFiles(openFile); Store.setActiveFiles(openFile);
...@@ -92,38 +111,43 @@ export default { ...@@ -92,38 +111,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"
: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,10 +41,13 @@ export default RepoTab; ...@@ -39,10 +41,13 @@ export default RepoTab;
</script> </script>
<template> <template>
<li @click="tabClicked(tab)"> <li
<a :class="{ active : tab.active }"
href="#0" @click="tabClicked(tab)"
class="close" >
<button
type="button"
class="close-btn"
@click.stop.prevent="closeTab(tab)" @click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel"> :aria-label="closeLabel">
<i <i
...@@ -50,7 +55,7 @@ export default RepoTab; ...@@ -50,7 +55,7 @@ export default RepoTab;
:class="changedClass" :class="changedClass"
aria-hidden="true"> aria-hidden="true">
</i> </i>
</a> </button>
<a <a
href="#" href="#"
...@@ -59,5 +64,5 @@ export default RepoTab; ...@@ -59,5 +64,5 @@ export default RepoTab;
@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, data: () => Store,
};
methods: {
tabClosed(file) {
Store.removeFromOpenedFiles(file);
},
},
};
export default RepoTabs;
</script> </script>
<template> <template>
<ul id="tabs"> <ul
id="tabs"
class="list-unstyled"
>
<repo-tab <repo-tab
v-for="tab in openedFiles" v-for="tab in openedFiles"
:key="tab.id" :key="tab.id"
:tab="tab" :tab="tab"
:class="{'active' : tab.active}"
@tabclosed="tabClosed"
/> />
<li class="tabs-divider" /> <li class="tabs-divider" />
</ul> </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
...@@ -59,12 +56,17 @@ const RepoHelper = { ...@@ -59,12 +56,17 @@ const RepoHelper = {
setDirectoryOpen(tree, title) { setDirectoryOpen(tree, title) {
const file = tree; const file = tree;
if (!file) return undefined; if (!file) return;
file.opened = true; file.opened = true;
file.icon = 'fa-folder-open';
RepoHelper.updateHistoryEntry(file.url, title); RepoHelper.updateHistoryEntry(file.url, title);
return file; },
setDirectoryToClosed(entry) {
const dir = entry;
dir.opened = false;
dir.tree = [];
}, },
isRenderable() { isRenderable() {
...@@ -81,63 +83,20 @@ const RepoHelper = { ...@@ -81,63 +83,20 @@ const RepoHelper = {
.catch(RepoHelper.loadingError); .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) {
let file = treeOrFile; let file = treeOrFile;
if (!Store.files.length) {
Store.loading.tree = true;
}
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.isRoot = convertPermissionToBoolean(response.headers['is-root']);
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,8 +104,7 @@ const RepoHelper = { ...@@ -145,8 +104,7 @@ 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;
...@@ -154,24 +112,20 @@ const RepoHelper = { ...@@ -154,24 +112,20 @@ const RepoHelper = {
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 (!file) {
if (Store.files.length === 0) { Store.files = this.dataToListOfFiles(data);
const parentURL = Service.blobURLtoParentTree(Service.url);
Service.url = parentURL;
RepoHelper.getContent();
}
} else { } else {
// it's a tree file.tree = this.dataToListOfFiles(data, file.level + 1);
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);
...@@ -190,57 +144,57 @@ const RepoHelper = { ...@@ -190,57 +144,57 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
serializeBlob(blob) { serializeBlob(blob, level) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); return RepoHelper.serializeRepoEntity('blob', blob, level);
simpleBlob.lastCommitMessage = blob.last_commit.message;
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
simpleBlob.loading = false;
return simpleBlob;
}, },
serializeTree(tree) { serializeTree(tree, level) {
return RepoHelper.serializeRepoEntity('tree', tree); return RepoHelper.serializeRepoEntity('tree', tree, level);
}, },
serializeSubmodule(submodule) { serializeSubmodule(submodule, level) {
return RepoHelper.serializeRepoEntity('submodule', submodule); return RepoHelper.serializeRepoEntity('submodule', submodule, level);
}, },
serializeRepoEntity(type, entity) { serializeRepoEntity(type, entity, level = 0) {
const { url, name, icon, last_commit } = entity; const { url, name, icon, last_commit } = entity;
const returnObj = { const returnObj = {
type, type,
name, name,
url, url,
level,
icon: `fa-${icon}`, icon: `fa-${icon}`,
level: 0, tree: [],
loading: false, loading: false,
opened: false,
}; };
if (entity.last_commit) { // eslint-disable-next-line camelcase
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`; if (last_commit) {
returnObj.lastCommit = {
url: `${Store.projectUrl}/commit/${last_commit.id}`,
message: last_commit.message,
updatedAt: last_commit.committed_date,
};
} else { } else {
returnObj.lastCommitUrl = ''; returnObj.lastCommit = {};
} }
return returnObj; return returnObj;
}, },
scrollTabsRight() { scrollTabsRight() {
// wait for the transition. 0.1 seconds.
setTimeout(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
if (!tabs) return; if (!tabs) return;
tabs.scrollLeft = tabs.scrollWidth; 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.serializeTree(tree, level)),
...trees.map(tree => RepoHelper.serializeTree(tree)), ...blobs.map(blob => RepoHelper.serializeBlob(blob, level)),
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule, 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,7 @@ function setInitialStore(data) { ...@@ -33,6 +34,7 @@ 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.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();
......
...@@ -8,7 +8,6 @@ const RepoStore = { ...@@ -8,7 +8,6 @@ const RepoStore = {
canCommit: false, canCommit: false,
onTopOfBranch: false, onTopOfBranch: false,
editMode: false, editMode: false,
isTree: false,
isRoot: false, isRoot: false,
prevURL: '', prevURL: '',
projectId: '', projectId: '',
...@@ -72,10 +71,6 @@ const RepoStore = { ...@@ -72,10 +71,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 +124,6 @@ const RepoStore = { ...@@ -129,30 +124,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 +157,7 @@ const RepoStore = { ...@@ -186,6 +157,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 {
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 { .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;
font-weight: $gl-font-weight-bold;
color: $gray-darkest;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: middle; 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),
......
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();
});
});
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