Commit 552ffea8 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'ee-repo-fixes' into 'master'

Many Repo Fixes -- EE merge edition

See merge request !2679
parents ea20437d c269a1b4
...@@ -98,7 +98,6 @@ const Api = { ...@@ -98,7 +98,6 @@ const Api = {
}, },
commitMultiple(id, data, callback) { commitMultiple(id, data, callback) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath) const url = Api.buildUrl(Api.commitPath)
.replace(':id', id); .replace(':id', id);
return $.ajax({ return $.ajax({
......
...@@ -138,11 +138,11 @@ import Cookies from 'js-cookie'; ...@@ -138,11 +138,11 @@ import Cookies from 'js-cookie';
var $form = $dropdown.closest('form'); var $form = $dropdown.closest('form');
var $visit = $dropdown.data('visit'); var $visit = $dropdown.data('visit');
var shouldVisit = typeof $visit === 'undefined' ? true : $visit; var shouldVisit = $visit ? true : $visit;
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&'; var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) { if (shouldVisit) {
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
} }
} }
} }
......
...@@ -14,13 +14,13 @@ export default { ...@@ -14,13 +14,13 @@ export default {
data: () => Store, data: () => Store,
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
'repo-sidebar': RepoSidebar, RepoSidebar,
'repo-tabs': RepoTabs, RepoTabs,
'repo-file-buttons': RepoFileButtons, RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader, 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
'repo-commit-section': RepoCommitSection, RepoCommitSection,
'popup-dialog': PopupDialog, PopupDialog,
'repo-preview': RepoPreview, RepoPreview,
}, },
mounted() { mounted() {
...@@ -28,12 +28,12 @@ export default { ...@@ -28,12 +28,12 @@ export default {
}, },
methods: { methods: {
dialogToggled(toggle) { toggleDialogOpen(toggle) {
this.dialog.open = toggle; this.dialog.open = toggle;
}, },
dialogSubmitted(status) { dialogSubmitted(status) {
this.dialog.open = false; this.toggleDialogOpen(false);
this.dialog.status = status; this.dialog.status = status;
}, },
...@@ -43,21 +43,25 @@ export default { ...@@ -43,21 +43,25 @@ export default {
</script> </script>
<template> <template>
<div class="repository-view tree-content-holder"> <div class="repository-view tree-content-holder">
<repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}"> <repo-sidebar/><div v-if="isMini"
<repo-tabs/> class="panel-right"
<component :is="currentBlobView" class="blob-viewer-container"></component> :class="{'edit-mode': editMode}">
<repo-file-buttons/> <repo-tabs/>
<component
:is="currentBlobView"
class="blob-viewer-container"/>
<repo-file-buttons/>
</div>
<repo-commit-section/>
<popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
</div> </div>
<repo-commit-section/>
<popup-dialog
:primary-button-label="__('Discard changes')"
:open="dialog.open"
kind="warning"
:title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')"
@toggle="dialogToggled"
@submit="dialogSubmitted"
/>
</div>
</template> </template>
...@@ -2,18 +2,20 @@ ...@@ -2,18 +2,20 @@
/* global Flash */ /* global Flash */
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoCommitSection = { export default {
data: () => Store, data: () => Store,
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
},
branchPaths() { branchPaths() {
const branch = Helper.getBranch(); return this.changedFiles.map(f => f.path);
return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
}, },
cantCommitYet() { cantCommitYet() {
...@@ -28,11 +30,10 @@ const RepoCommitSection = { ...@@ -28,11 +30,10 @@ const RepoCommitSection = {
methods: { methods: {
makeCommit() { makeCommit() {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const branch = Helper.getBranch();
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
action: 'update', action: 'update',
file_path: Helper.getFilePathFromFullPath(f.url, branch), file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
const payload = { const payload = {
...@@ -47,51 +48,80 @@ const RepoCommitSection = { ...@@ -47,51 +48,80 @@ const RepoCommitSection = {
resetCommitState() { resetCommitState() {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.changedFiles = []; this.changedFiles = [];
this.openedFiles = [];
this.commitMessage = ''; this.commitMessage = '';
this.editMode = false; this.editMode = false;
$('html, body').animate({ scrollTop: 0 }, 'fast'); window.scrollTo(0, 0);
}, },
}, },
}; };
export default RepoCommitSection;
</script> </script>
<template> <template>
<div id="commit-area" v-if="isCommitable && changedFiles.length" > <div
<form class="form-horizontal"> v-if="showCommitable"
id="commit-area">
<form
class="form-horizontal"
@submit.prevent="makeCommit">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label> <label class="col-md-4 control-label staged-files">
<div class="col-md-4"> Staged files ({{changedFiles.length}})
</label>
<div class="col-md-6">
<ul class="list-unstyled changed-files"> <ul class="list-unstyled changed-files">
<li v-for="file in branchPaths" :key="file.id"> <li
<span class="help-block">{{file}}</span> v-for="branchPath in branchPaths"
:key="branchPath">
<span class="help-block">
{{branchPath}}
</span>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Textarea
-->
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label" for="commit-message">Commit message</label> <label
<div class="col-md-4"> class="col-md-4 control-label"
<textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea> for="commit-message">
Commit message
</label>
<div class="col-md-6">
<textarea
id="commit-message"
class="form-control"
name="commit-message"
v-model="commitMessage">
</textarea>
</div> </div>
</div> </div>
<!-- Button Drop Down
-->
<div class="form-group target-branch"> <div class="form-group target-branch">
<label class="col-md-4 control-label" for="target-branch">Target branch</label> <label
<div class="col-md-4"> class="col-md-4 control-label"
<span class="help-block">{{targetBranch}}</span> for="target-branch">
Target branch
</label>
<div class="col-md-6">
<span class="help-block">
{{targetBranch}}
</span>
</div> </div>
</div> </div>
<div class="col-md-offset-4 col-md-4"> <div class="col-md-offset-4 col-md-6">
<button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit"> <button
<i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i> ref="submitCommit"
<span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span> type="submit"
:disabled="cantCommitYet"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading">
</i>
<span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}}
</span>
</button> </button>
</div> </div>
</fieldset> </fieldset>
......
...@@ -10,12 +10,15 @@ export default { ...@@ -10,12 +10,15 @@ export default {
return this.editMode ? this.__('Cancel edit') : this.__('Edit'); return this.editMode ? this.__('Cancel edit') : this.__('Edit');
}, },
buttonIcon() { showButton() {
return this.editMode ? [] : ['fa', 'fa-pencil']; return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
}, },
}, },
methods: { methods: {
editClicked() { editCancelClicked() {
if (this.changedFiles.length) { if (this.changedFiles.length) {
this.dialog.open = true; this.dialog.open = true;
return; return;
...@@ -23,27 +26,33 @@ export default { ...@@ -23,27 +26,33 @@ export default {
this.editMode = !this.editMode; this.editMode = !this.editMode;
Store.toggleBlobView(); Store.toggleBlobView();
}, },
toggleProjectRefsForm() {
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
}, },
watch: { watch: {
editMode() { editMode() {
if (this.editMode) { this.toggleProjectRefsForm();
$('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show();
$('.project-refs-target-form').show();
} else {
$('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide();
$('.project-refs-target-form').hide();
}
}, },
}, },
}; };
</script> </script>
<template> <template>
<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary"> <button
<i :class="buttonIcon"></i> v-if="showButton"
<span>{{buttonLabel}}</span> class="btn btn-default"
type="button"
@click.prevent="editCancelClicked">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button> </button>
</template> </template>
...@@ -8,38 +8,39 @@ const RepoEditor = { ...@@ -8,38 +8,39 @@ const RepoEditor = {
data: () => Store, data: () => Store,
destroyed() { destroyed() {
// this.monacoInstance.getModels().forEach((m) => { if (Helper.monacoInstance) {
// m.dispose(); Helper.monacoInstance.destroy();
// }); }
this.monacoInstance.destroy();
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile.raw_path) Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data; Store.activeFile.plain = rawResponse.data;
const monacoInstance = this.monaco.editor.create(this.$el, { const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null, model: null,
readOnly: false, readOnly: false,
contextmenu: false, contextmenu: false,
}); });
Store.monacoInstance = monacoInstance; Helper.monacoInstance = monacoInstance;
this.addMonacoEvents(); this.addMonacoEvents();
const languages = this.monaco.languages.getLanguages(); this.setupEditor();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); })
this.showHide(); .catch(Helper.loadingError);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}).catch(Helper.loadingError);
}, },
methods: { methods: {
setupEditor() {
this.showHide();
Helper.setMonacoModelFromLanguage();
},
showHide() { showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none'; this.$el.style.display = 'none';
...@@ -49,41 +50,36 @@ const RepoEditor = { ...@@ -49,41 +50,36 @@ const RepoEditor = {
}, },
addMonacoEvents() { addMonacoEvents() {
this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
}, },
onMonacoEditorKeysPressed() { onMonacoEditorKeysPressed() {
Store.setActiveFileContents(this.monacoInstance.getValue()); Store.setActiveFileContents(Helper.monacoInstance.getValue());
}, },
onMonacoEditorMouseUp(e) { onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber; const lineNumber = e.target.position.lineNumber;
if (e.target.element.className === 'line-numbers') { if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`; location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber; Store.activeLine = lineNumber;
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
} }
}, },
}, },
watch: { watch: {
activeLine() {
this.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
},
activeFileLabel() {
this.showHide();
},
dialog: { dialog: {
handler(obj) { handler(obj) {
const newObj = obj; const newObj = obj;
if (newObj.status) { if (newObj.status) {
newObj.status = false; newObj.status = false;
this.openedFiles.map((file) => { this.openedFiles = this.openedFiles.map((file) => {
const f = file; const f = file;
if (f.active) { if (f.active) {
this.blobRaw = f.plain; this.blobRaw = f.plain;
...@@ -94,35 +90,21 @@ const RepoEditor = { ...@@ -94,35 +90,21 @@ const RepoEditor = {
return f; return f;
}); });
this.editMode = false; this.editMode = false;
Store.toggleBlobView();
} }
}, },
deep: true, deep: true,
}, },
isTree() {
this.showHide();
},
openedFiles() {
this.showHide();
},
binary() {
this.showHide();
},
blobRaw() { blobRaw() {
this.showHide(); if (Helper.monacoInstance && !this.isTree) {
this.setupEditor();
if (this.isTree) return; }
},
this.monacoInstance.setModel(null); },
computed: {
const languages = this.monaco.languages.getLanguages(); shouldHideEditor() {
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}, },
}, },
}; };
...@@ -131,5 +113,5 @@ export default RepoEditor; ...@@ -131,5 +113,5 @@ export default RepoEditor;
</script> </script>
<template> <template>
<div id="ide"></div> <div id="ide" v-if='!shouldHideEditor'></div>
</template> </template>
...@@ -33,6 +33,26 @@ const RepoFile = { ...@@ -33,6 +33,26 @@ const RepoFile = {
canShowFile() { canShowFile() {
return !this.loading.tree || this.hasFiles; 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: { methods: {
...@@ -46,21 +66,42 @@ export default RepoFile; ...@@ -46,21 +66,42 @@ export default RepoFile;
</script> </script>
<template> <template>
<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}"> <tr
<td @click.prevent="linkClicked(file)"> v-if="canShowFile"
<i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i> class="file"
<i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i> :class="activeFileClass"
<a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a> @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> </td>
<td v-if="!isMini" class="hidden-sm hidden-xs"> <template v-if="!isMini">
<div class="commit-message"> <td class="hidden-sm hidden-xs">
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a> <div class="commit-message">
</div> <a @click.stop :href="file.lastCommitUrl">
</td> {{file.lastCommitMessage}}
</a>
</div>
</td>
<td v-if="!isMini" class="hidden-xs"> <td class="hidden-xs">
<span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span> <span
</td> class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)">
{{timeFormated(file.lastCommitUpdate)}}
</span>
</td>
</template>
</tr> </tr>
</template> </template>
...@@ -15,7 +15,7 @@ const RepoFileButtons = { ...@@ -15,7 +15,7 @@ const RepoFileButtons = {
}, },
canPreview() { canPreview() {
return Helper.isKindaBinary(); return Helper.isRenderable();
}, },
}, },
...@@ -28,15 +28,42 @@ export default RepoFileButtons; ...@@ -28,15 +28,42 @@ export default RepoFileButtons;
</script> </script>
<template> <template>
<div id="repo-file-buttons" v-if="isMini"> <div id="repo-file-buttons">
<a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a> <a
:href="activeFile.raw_path"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
{{rawDownloadButtonLabel}}
</a>
<div class="btn-group" role="group" aria-label="File actions"> <div
<a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a> class="btn-group"
<a :href="activeFile.commits_path" class="btn btn-default history">History</a> role="group"
<a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a> aria-label="File actions">
</div> <a
:href="activeFile.blame_path"
class="btn btn-default blame">
Blame
</a>
<a
:href="activeFile.commits_path"
class="btn btn-default history">
History
</a>
<a
:href="activeFile.permalink"
class="btn btn-default permalink">
Permalink
</a>
</div>
<a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a> <a
</div> v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div>
</template> </template>
...@@ -17,7 +17,7 @@ export default RepoFileOptions; ...@@ -17,7 +17,7 @@ export default RepoFileOptions;
</script> </script>
<template> <template>
<tr v-if="isMini" class="repo-file-options"> <tr v-if="isMini" class="repo-file-options">
<td> <td>
<span class="title">{{projectName}}</span> <span class="title">{{projectName}}</span>
</td> </td>
......
...@@ -18,9 +18,15 @@ const RepoLoadingFile = { ...@@ -18,9 +18,15 @@ const RepoLoadingFile = {
}, },
}, },
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
},
},
methods: { methods: {
lineOfCode(n) { lineOfCode(n) {
return `line-of-code-${n}`; return `skeleton-line-${n}`;
}, },
}, },
}; };
...@@ -29,23 +35,42 @@ export default RepoLoadingFile; ...@@ -29,23 +35,42 @@ export default RepoLoadingFile;
</script> </script>
<template> <template>
<tr v-if="loading.tree && !hasFiles" class="loading-file"> <tr
<td> v-if="showGhostLines"
<div class="animation-container animation-container-small"> class="loading-file">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> <td>
</div> <div
</td> class="animation-container animation-container-small">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs"> <td
<div class="animation-container"> v-if="!isMini"
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> class="hidden-sm hidden-xs">
</div> <div class="animation-container">
</td> <div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
<td v-if="!isMini" class="hidden-xs"> <td
<div class="animation-container animation-container-small"> v-if="!isMini"
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> class="hidden-xs">
</div> <div class="animation-container animation-container-small">
</td> <div
</tr> v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
</tr>
</template> </template>
<script> <script>
import RepoMixin from '../mixins/repo_mixin';
const RepoPreviousDirectory = { const RepoPreviousDirectory = {
props: { props: {
prevUrl: { prevUrl: {
...@@ -7,6 +9,14 @@ const RepoPreviousDirectory = { ...@@ -7,6 +9,14 @@ const RepoPreviousDirectory = {
}, },
}, },
mixins: [RepoMixin],
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
},
},
methods: { methods: {
linkClicked(file) { linkClicked(file) {
this.$emit('linkclicked', file); this.$emit('linkclicked', file);
...@@ -19,8 +29,10 @@ export default RepoPreviousDirectory; ...@@ -19,8 +29,10 @@ export default RepoPreviousDirectory;
<template> <template>
<tr class="prev-directory"> <tr class="prev-directory">
<td colspan="3"> <td
<a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a> :colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)">
<a :href="prevUrl">..</a>
</td> </td>
</tr> </tr>
</template> </template>
...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store'; ...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() { mounted() {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}, },
computed: { computed: {
html() { html() {
...@@ -12,10 +12,16 @@ export default { ...@@ -12,10 +12,16 @@ export default {
}, },
}, },
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}); });
}, },
}, },
...@@ -24,9 +30,23 @@ export default { ...@@ -24,9 +30,23 @@ export default {
<template> <template>
<div> <div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div> <div
<div v-if="activeFile.render_error" class="vertical-center render-error"> v-if="!activeFile.render_error"
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p> v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div> </div>
</div> </div>
</template> </template>
...@@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue'; ...@@ -8,7 +8,7 @@ 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';
const RepoSidebar = { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
'repo-file-options': RepoFileOptions, 'repo-file-options': RepoFileOptions,
...@@ -33,40 +33,36 @@ const RepoSidebar = { ...@@ -33,40 +33,36 @@ const RepoSidebar = {
}); });
}, },
linkClicked(clickedFile) { fileClicked(clickedFile) {
let url = '';
let file = clickedFile; let file = clickedFile;
if (typeof file === 'object') { if (file.loading) return;
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
url = file.url; Service.url = file.url;
Service.url = url; Helper.getContent(file)
// I need to refactor this to do the `then` here. .then(() => {
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false; file.loading = false;
Helper.scrollTabsRight(); Helper.scrollTabsRight();
}); })
} .catch(Helper.loadingError);
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
} }
}, },
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
}, },
}; };
export default RepoSidebar;
</script> </script>
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak> <div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table"> <table class="table">
<thead v-if="!isMini"> <thead v-if="!isMini">
<tr> <tr>
...@@ -82,7 +78,7 @@ export default RepoSidebar; ...@@ -82,7 +78,7 @@ export default RepoSidebar;
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="isRoot"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/> @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file <repo-loading-file
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
...@@ -94,7 +90,7 @@ export default RepoSidebar; ...@@ -94,7 +90,7 @@ export default RepoSidebar;
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini" :is-mini="isMini"
@linkclicked="linkClicked(file)" @linkclicked="fileClicked(file)"
:is-tree="isTree" :is-tree="isTree"
:has-files="!!files.length" :has-files="!!files.length"
:active-file="activeFile"/> :active-file="activeFile"/>
......
...@@ -10,10 +10,16 @@ const RepoTab = { ...@@ -10,10 +10,16 @@ const RepoTab = {
}, },
computed: { computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times': !this.tab.changed, 'fa-times close-icon': !this.tab.changed,
'fa-circle': this.tab.changed, 'fa-circle unsaved-icon': this.tab.changed,
}; };
return tabChangedObj; return tabChangedObj;
}, },
...@@ -22,9 +28,9 @@ const RepoTab = { ...@@ -22,9 +28,9 @@ const RepoTab = {
methods: { methods: {
tabClicked: Store.setActiveFiles, tabClicked: Store.setActiveFiles,
xClicked(file) { closeTab(file) {
if (file.changed) return; if (file.changed) return;
this.$emit('xclicked', file); this.$emit('tabclosed', file);
}, },
}, },
}; };
...@@ -33,13 +39,25 @@ export default RepoTab; ...@@ -33,13 +39,25 @@ export default RepoTab;
</script> </script>
<template> <template>
<li> <li @click="tabClicked(tab)">
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> <a
<i class="fa" :class="changedClass"></i> href="#0"
class="close"
@click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a> </a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> <a
href="#"
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li> </li>
</template> </template>
<script> <script>
import Vue from 'vue';
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';
...@@ -14,30 +13,24 @@ const RepoTabs = { ...@@ -14,30 +13,24 @@ const RepoTabs = {
data: () => Store, data: () => Store,
methods: { methods: {
isOverflow() { tabClosed(file) {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) {
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
}, },
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
}; };
export default RepoTabs; export default RepoTabs;
</script> </script>
<template> <template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}"> <ul id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> <repo-tab
v-for="tab in openedFiles"
:key="tab.id"
:tab="tab"
:class="{'active' : tab.active}"
@tabclosed="tabClosed"
/>
<li class="tabs-divider" /> <li class="tabs-divider" />
</ul> </ul>
</template> </template>
/* global monaco */ /* global monaco */
import RepoEditor from '../components/repo_editor.vue'; import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
function repoEditorLoader() { function repoEditorLoader() {
Store.monacoLoading = true; Store.monacoLoading = true;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
Store.monaco = monaco; Helper.monaco = monaco;
Store.monacoLoading = false; Store.monacoLoading = false;
resolve(RepoEditor); resolve(RepoEditor);
}, reject); }, () => {
Store.monacoLoading = false;
reject();
});
}); });
} }
......
...@@ -4,6 +4,8 @@ import Store from '../stores/repo_store'; ...@@ -4,6 +4,8 @@ import Store from '../stores/repo_store';
import '../../flash'; import '../../flash';
const RepoHelper = { const RepoHelper = {
monacoInstance: null,
getDefaultActiveFile() { getDefaultActiveFile() {
return { return {
active: true, active: true,
...@@ -33,19 +35,23 @@ const RepoHelper = { ...@@ -33,19 +35,23 @@ const RepoHelper = {
? window.performance ? window.performance
: Date, : Date,
getBranch() { getFileExtension(fileName) {
return $('button.dropdown-menu-toggle').attr('data-ref'); return fileName.split('.').pop();
}, },
getLanguageIDForFile(file, langs) { getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop(); const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs); const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext'; return foundLang ? foundLang.id : 'plaintext';
}, },
getFilePathFromFullPath(fullPath, branch) { setMonacoModelFromLanguage() {
return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1]; RepoHelper.monacoInstance.setModel(null);
const languages = RepoHelper.monaco.languages.getLanguages();
const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
RepoHelper.monacoInstance.setModel(newModel);
}, },
findLanguage(ext, langs) { findLanguage(ext, langs) {
...@@ -58,11 +64,11 @@ const RepoHelper = { ...@@ -58,11 +64,11 @@ const RepoHelper = {
file.opened = true; file.opened = true;
file.icon = 'fa-folder-open'; file.icon = 'fa-folder-open';
RepoHelper.toURL(file.url, file.name); RepoHelper.updateHistoryEntry(file.url, file.name);
return file; return file;
}, },
isKindaBinary() { isRenderable() {
const okExts = ['md', 'svg']; const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1; return okExts.indexOf(Store.activeFile.extension) > -1;
}, },
...@@ -76,22 +82,8 @@ const RepoHelper = { ...@@ -76,22 +82,8 @@ const RepoHelper = {
.catch(RepoHelper.loadingError); .catch(RepoHelper.loadingError);
}, },
toggleFakeTab(loading, file) { // when you open a directory you need to put the directory files under
if (loading) return Store.addPlaceholderFile(); // the directory... This will merge the list of the current directory and the new list.
return Store.removeFromOpenedFiles(file);
},
setLoading(loading, file) {
if (Service.url.indexOf('blob') > -1) {
Store.loading.blob = loading;
return RepoHelper.toggleFakeTab(loading, file);
}
if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
return undefined;
},
getNewMergedList(inDirectory, currentList, newList) { getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive); const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted; if (!inDirectory) return newListSorted;
...@@ -100,6 +92,9 @@ const RepoHelper = { ...@@ -100,6 +92,9 @@ const RepoHelper = {
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); 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) { mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => { newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1; const fileIndex = indexOfFile + 1;
...@@ -135,21 +130,17 @@ const RepoHelper = { ...@@ -135,21 +130,17 @@ const RepoHelper = {
return isRoot; return isRoot;
}, },
getContent(treeOrFile, cb) { getContent(treeOrFile) {
let file = treeOrFile; let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
// RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
if (data.binary) { if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
...@@ -188,9 +179,8 @@ const RepoHelper = { ...@@ -188,9 +179,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url; newFile.url = file.url;
if (newFile.render_error === 'too_large') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
newFile.newContent = ''; newFile.newContent = '';
...@@ -199,10 +189,6 @@ const RepoHelper = { ...@@ -199,10 +189,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) { serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message; simpleBlob.lastCommitMessage = blob.last_commit.message;
...@@ -226,7 +212,7 @@ const RepoHelper = { ...@@ -226,7 +212,7 @@ const RepoHelper = {
type, type,
name, name,
url, url,
icon: RepoHelper.toFA(icon), icon: `fa-${icon}`,
level: 0, level: 0,
loading: false, loading: false,
}; };
...@@ -244,42 +230,24 @@ const RepoHelper = { ...@@ -244,42 +230,24 @@ const RepoHelper = {
setTimeout(() => { setTimeout(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
if (!tabs) return; if (!tabs) return;
tabs.scrollLeft = 12000; tabs.scrollLeft = tabs.scrollWidth;
}, 200); }, 200);
}, },
dataToListOfFiles(data) { dataToListOfFiles(data) {
const a = []; const { blobs, trees, submodules } = data;
return [
// push in blobs ...blobs.map(blob => RepoHelper.serializeBlob(blob)),
data.blobs.forEach((blob) => { ...trees.map(tree => RepoHelper.serializeTree(tree)),
a.push(RepoHelper.serializeBlob(blob)); ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
}); ];
data.trees.forEach((tree) => {
a.push(RepoHelper.serializeTree(tree));
});
data.submodules.forEach((submodule) => {
a.push(RepoHelper.serializeSubmodule(submodule));
});
return a;
}, },
genKey() { genKey() {
return RepoHelper.Time.now().toFixed(3); return RepoHelper.Time.now().toFixed(3);
}, },
getStateKey() { updateHistoryEntry(url, title) {
return RepoHelper.key;
},
setStateKey(key) {
RepoHelper.key = key;
},
toURL(url, title) {
const history = window.history; const history = window.history;
RepoHelper.key = RepoHelper.genKey(); RepoHelper.key = RepoHelper.genKey();
...@@ -296,7 +264,7 @@ const RepoHelper = { ...@@ -296,7 +264,7 @@ const RepoHelper = {
}, },
loadingError() { loadingError() {
Flash('Unable to load the file at this time.'); Flash('Unable to load this content at this time.');
}, },
}; };
......
...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue'; ...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
$('.project-refs-target-form').hide(); $('.js-tree-ref-target-holder').hide();
$('.fa-long-arrow-right').hide();
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {
...@@ -34,6 +33,8 @@ function setInitialStore(data) { ...@@ -34,6 +33,8 @@ function setInitialStore(data) {
Store.projectId = data.projectId; Store.projectId = data.projectId;
Store.projectName = data.projectName; Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable(); Store.checkIsCommitable();
} }
...@@ -44,6 +45,9 @@ function initRepo(el) { ...@@ -44,6 +45,9 @@ function initRepo(el) {
components: { components: {
repo: Repo, repo: Repo,
}, },
render(createElement) {
return createElement('repo');
},
}); });
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = { const RepoService = {
url: '', url: '',
...@@ -12,16 +13,9 @@ const RepoService = { ...@@ -12,16 +13,9 @@ const RepoService = {
}, },
richExtensionRegExp: /md/, richExtensionRegExp: /md/,
checkCurrentBranchIsCommitable() {
const url = Store.service.refsUrl;
return axios.get(url, { params: {
ref: Store.currentBranch,
search: Store.currentBranch,
} });
},
getRaw(url) { getRaw(url) {
return axios.get(url, { return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
}, },
...@@ -36,7 +30,7 @@ const RepoService = { ...@@ -36,7 +30,7 @@ const RepoService = {
}, },
urlIsRichBlob(url = this.url) { urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop(); const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension); return this.richExtensionRegExp.test(extension);
}, },
...@@ -73,7 +67,11 @@ const RepoService = { ...@@ -73,7 +67,11 @@ const RepoService = {
commitFiles(payload, cb) { commitFiles(payload, cb) {
Api.commitMultiple(Store.projectId, payload, (data) => { Api.commitMultiple(Store.projectId, payload, (data) => {
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); if (data.short_id && data.stats) {
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
} else {
Flash(data.message);
}
cb(); cb();
}); });
}, },
......
...@@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper'; ...@@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
ideEl: {},
monaco: {}, monaco: {},
monacoLoading: false, monacoLoading: false,
monacoInstance: {},
service: '', service: '',
editor: '', canCommit: false,
sidebar: '', onTopOfBranch: false,
editMode: false, editMode: false,
isTree: false, isTree: false,
isRoot: false, isRoot: false,
...@@ -17,19 +15,10 @@ const RepoStore = { ...@@ -17,19 +15,10 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '', blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false, submitCommitsLoading: false,
binaryLoaded: false,
dialog: { dialog: {
open: false, open: false,
title: '', title: '',
...@@ -45,9 +34,6 @@ const RepoStore = { ...@@ -45,9 +34,6 @@ const RepoStore = {
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch', targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: { binaryTypes: {
png: false, png: false,
md: false, md: false,
...@@ -58,7 +44,6 @@ const RepoStore = { ...@@ -58,7 +44,6 @@ const RepoStore = {
tree: false, tree: false,
blob: false, blob: false,
}, },
readOnly: true,
resetBinaryTypes() { resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => { Object.keys(RepoStore.binaryTypes).forEach((key) => {
...@@ -68,14 +53,7 @@ const RepoStore = { ...@@ -68,14 +53,7 @@ const RepoStore = {
// mutations // mutations
checkIsCommitable() { checkIsCommitable() {
RepoStore.service.checkCurrentBranchIsCommitable() RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
.then((data) => {
// you shouldn't be able to make commits on commits or tags.
const { Branches, Commits, Tags } = data.data;
if (Branches && Branches.length) RepoStore.isCommitable = true;
if (Commits && Commits.length) RepoStore.isCommitable = false;
if (Tags && Tags.length) RepoStore.isCommitable = false;
}).catch(() => Flash('Failed to check if branch can be committed to.'));
}, },
addFilesToDirectory(inDirectory, currentList, newList) { addFilesToDirectory(inDirectory, currentList, newList) {
...@@ -96,7 +74,6 @@ const RepoStore = { ...@@ -96,7 +74,6 @@ const RepoStore = {
if (file.binary) { if (file.binary) {
RepoStore.blobRaw = file.base64; RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
...@@ -107,7 +84,7 @@ const RepoStore = { ...@@ -107,7 +84,7 @@ const RepoStore = {
}).catch(Helper.loadingError); }).catch(Helper.loadingError);
} }
if (!file.loading) Helper.toURL(file.url, file.name); if (!file.loading) Helper.updateHistoryEntry(file.url, file.name);
RepoStore.binary = file.binary; RepoStore.binary = file.binary;
}, },
...@@ -134,15 +111,15 @@ const RepoStore = { ...@@ -134,15 +111,15 @@ const RepoStore = {
removeChildFilesOfTree(tree) { removeChildFilesOfTree(tree) {
let foundTree = false; let foundTree = false;
const treeToClose = tree; const treeToClose = tree;
let wereDone = false; let canStopSearching = false;
RepoStore.files = RepoStore.files.filter((file) => { RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url; const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree // if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
wereDone = true; canStopSearching = true;
return true; return true;
} }
if (wereDone) return true; if (canStopSearching) return true;
if (isItTheTreeWeWant) foundTree = true; if (isItTheTreeWeWant) foundTree = true;
...@@ -159,8 +136,8 @@ const RepoStore = { ...@@ -159,8 +136,8 @@ const RepoStore = {
if (file.type === 'tree') return; if (file.type === 'tree') return;
let foundIndex; let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
if (openedFile.url === file.url) foundIndex = i; if (openedFile.path === file.path) foundIndex = i;
return openedFile.url !== file.url; return openedFile.path !== file.path;
}); });
// now activate the right tab based on what you closed. // now activate the right tab based on what you closed.
...@@ -174,36 +151,16 @@ const RepoStore = { ...@@ -174,36 +151,16 @@ const RepoStore = {
return; return;
} }
if (foundIndex) { if (foundIndex && foundIndex > 0) {
if (foundIndex > 0) { RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
}
} }
}, },
addPlaceholderFile() {
const randomURL = Helper.Time.now();
const newFakeFile = {
active: false,
binary: true,
type: 'blob',
loading: true,
mime_type: 'loading',
name: 'loading',
url: randomURL,
fake: true,
};
RepoStore.openedFiles.push(newFakeFile);
return newFakeFile;
},
addToOpenedFiles(file) { addToOpenedFiles(file) {
const openFile = file; const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles const openedFilesAlreadyExists = RepoStore.openedFiles
.some(openedFile => openedFile.url === openFile.url); .some(openedFile => openedFile.path === openFile.path);
if (openedFilesAlreadyExists) return; if (openedFilesAlreadyExists) return;
...@@ -238,4 +195,5 @@ const RepoStore = { ...@@ -238,4 +195,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview'; return RepoStore.currentBlobView === 'repo-preview';
}, },
}; };
export default RepoStore; export default RepoStore;
<script> <script>
const PopupDialog = { export default {
name: 'popup-dialog', name: 'popup-dialog',
props: { props: {
open: Boolean, title: {
title: String, type: String,
body: String, required: true,
},
body: {
type: String,
required: true,
},
kind: { kind: {
type: String, type: String,
required: false,
default: 'primary', default: 'primary',
}, },
closeButtonLabel: { closeButtonLabel: {
type: String, type: String,
required: false,
default: 'Cancel', default: 'Cancel',
}, },
primaryButtonLabel: { primaryButtonLabel: {
type: String, type: String,
default: 'Save changes', required: true,
}, },
}, },
computed: { computed: {
typeOfClass() { btnKindClass() {
const className = `btn-${this.kind}`; return {
const returnObj = {}; [`btn-${this.kind}`]: true,
returnObj[className] = true; };
return returnObj;
}, },
}, },
...@@ -33,33 +39,48 @@ const PopupDialog = { ...@@ -33,33 +39,48 @@ const PopupDialog = {
close() { close() {
this.$emit('toggle', false); this.$emit('toggle', false);
}, },
emitSubmit(status) {
yesClick() { this.$emit('submit', status);
this.$emit('submit', true);
},
noClick() {
this.$emit('submit', false);
}, },
}, },
}; };
export default PopupDialog;
</script> </script>
<template> <template>
<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog"> <div
<div class="modal-dialog" role="document"> class="modal popup-dialog"
role="dialog"
tabindex="-1">
<div
class="modal-dialog"
role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button"
class="close"
@click="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{this.title}}</h4> <h4 class="modal-title">{{this.title}}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>{{this.body}}</p> <p>{{this.body}}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button> <button
<button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button> type="button"
class="btn btn-default"
@click="emitSubmit(false)">
{{closeButtonLabel}}
</button>
<button
type="button"
class="btn"
:class="btnKindClass"
@click="emitSubmit(true)">
{{primaryButtonLabel}}
</button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -187,3 +187,81 @@ a { ...@@ -187,3 +187,81 @@ a {
.fade-in-full { .fade-in-full {
animation: fadeInFull $fade-in-duration 1; animation: fadeInFull $fade-in-duration 1;
} }
.animation-container {
background: $repo-editor-grey;
height: 40px;
overflow: hidden;
position: relative;
&.animation-container-small {
height: 12px;
}
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative;
}
div {
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.skeleton-line-1 {
left: 0;
top: 8px;
}
.skeleton-line-2 {
left: 150px;
top: 0;
height: 10px;
}
.skeleton-line-3 {
left: 0;
top: 23px;
}
.skeleton-line-4 {
left: 0;
top: 38px;
}
.skeleton-line-5 {
left: 200px;
top: 28px;
height: 10px;
}
.skeleton-line-6 {
top: 14px;
left: 230px;
height: 10px;
}
}
@keyframes blockTextShine {
0% {
transform: translateX(-468px);
}
100% {
transform: translateX(468px);
}
}
...@@ -117,10 +117,6 @@ body { ...@@ -117,10 +117,6 @@ body {
margin-top: $header-height + $performance-bar-height; margin-top: $header-height + $performance-bar-height;
} }
[v-cloak] {
display: none;
}
.vertical-center { .vertical-center {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
......
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity .5s; transition: opacity $sidebar-transition-duration;
} }
.monaco-loader { .monaco-loader {
...@@ -28,11 +28,6 @@ ...@@ -28,11 +28,6 @@
.project-refs-form, .project-refs-form,
.project-refs-target-form { .project-refs-target-form {
display: inline-block; display: inline-block;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
} }
.fade-enter, .fade-enter,
...@@ -90,7 +85,7 @@ ...@@ -90,7 +85,7 @@
} }
.blob-viewer-container { .blob-viewer-container {
height: calc(100vh - 63px); height: calc(100vh - 62px);
overflow: auto; overflow: auto;
} }
...@@ -114,6 +109,7 @@ ...@@ -114,6 +109,7 @@
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; white-space: nowrap;
cursor: pointer;
&.remove { &.remove {
animation: swipeRightDissapear ease-in 0.1s; animation: swipeRightDissapear ease-in 0.1s;
...@@ -133,10 +129,10 @@ ...@@ -133,10 +129,10 @@
a { a {
@include str-truncated(100px); @include str-truncated(100px);
color: $black; color: $black;
display: inline-block;
width: 100px; width: 100px;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
text-decoration: none;
&.close { &.close {
width: auto; width: auto;
...@@ -146,15 +142,15 @@ ...@@ -146,15 +142,15 @@
} }
} }
i.fa.fa-times, .close-icon,
i.fa.fa-circle { .unsaved-icon {
float: right; float: right;
margin-top: 3px; margin-top: 3px;
margin-left: 15px; margin-left: 15px;
color: $gray-darkest; color: $gray-darkest;
} }
i.fa.fa-circle { .unsaved-icon {
color: $brand-success; color: $brand-success;
} }
...@@ -204,7 +200,7 @@ ...@@ -204,7 +200,7 @@
background: $gray-light; background: $gray-light;
padding: 20px; padding: 20px;
span.help-block { .help-block {
padding-top: 7px; padding-top: 7px;
margin-top: 0; margin-top: 0;
} }
...@@ -232,6 +228,7 @@ ...@@ -232,6 +228,7 @@
vertical-align: top; vertical-align: top;
width: 20%; width: 20%;
border-right: 1px solid $white-normal; border-right: 1px solid $white-normal;
min-height: 475px;
height: calc(100vh + 20px); height: calc(100vh + 20px);
overflow: auto; overflow: auto;
} }
...@@ -261,7 +258,6 @@ ...@@ -261,7 +258,6 @@
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
color: $gray-darkest; color: $gray-darkest;
width: 185px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -270,7 +266,7 @@ ...@@ -270,7 +266,7 @@
} }
} }
.fa { .file-icon {
margin-right: 5px; margin-right: 5px;
} }
...@@ -280,118 +276,22 @@ ...@@ -280,118 +276,22 @@
} }
a { a {
@include str-truncated(250px);
color: $almost-black; color: $almost-black;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
ul {
list-style-type: none;
padding: 0;
li {
border-bottom: 1px solid $border-gray-normal;
padding: 10px 20px;
a {
color: $almost-black;
}
.fa {
font-size: $code_font_size;
margin-right: 5px;
}
}
}
}
}
.animation-container {
background: $repo-editor-grey;
height: 40px;
overflow: hidden;
position: relative;
&.animation-container-small {
height: 12px;
}
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative;
}
div {
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.line-of-code-1 {
left: 0;
top: 8px;
}
.line-of-code-2 {
left: 150px;
top: 0;
height: 10px;
}
.line-of-code-3 {
left: 0;
top: 23px;
}
.line-of-code-4 {
left: 0;
top: 38px;
}
.line-of-code-5 {
left: 200px;
top: 28px;
height: 10px;
}
.line-of-code-6 {
top: 14px;
left: 230px;
height: 10px;
} }
} }
.render-error { .render-error {
min-height: calc(100vh - 63px); min-height: calc(100vh - 62px);
p { p {
width: 100%; width: 100%;
} }
} }
@keyframes blockTextShine {
0% {
transform: translateX(-468px);
}
100% {
transform: translateX(468px);
}
}
@keyframes swipeRightAppear { @keyframes swipeRightAppear {
0% { 0% {
transform: scaleX(0.00); transform: scaleX(0.00);
......
...@@ -29,6 +29,10 @@ ...@@ -29,6 +29,10 @@
margin-right: 15px; margin-right: 15px;
} }
.tree-ref-target-holder {
display: inline-block;
}
.repo-breadcrumb { .repo-breadcrumb {
li:last-of-type { li:last-of-type {
position: relative; position: relative;
......
...@@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController
json = blob_json(@blob) json = blob_json(@blob)
return render_404 unless json return render_404 unless json
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
render json: json.merge( render json: json.merge(
path: blob.path, path: blob.path,
name: blob.name, name: blob.name,
...@@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController
raw_path: project_raw_path(project, @id), raw_path: project_raw_path(project, @id),
blame_path: project_blame_path(project, @id), blame_path: project_blame_path(project, @id),
commits_path: project_commits_path(project, @id), commits_path: project_commits_path(project, @id),
tree_path: project_tree_path(project, File.join(@ref, tree_path)),
permalink: project_blob_path(project, File.join(@commit.id, @path)) permalink: project_blob_path(project, File.join(@commit.id, @path))
) )
end end
......
# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`. # TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
class TreeRootEntity < Grape::Entity class TreeRootEntity < Grape::Entity
include RequestAwareEntity
expose :path expose :path
expose :trees, using: TreeEntity expose :trees, using: TreeEntity
expose :blobs, using: BlobEntity expose :blobs, using: BlobEntity
expose :submodules, using: SubmoduleEntity expose :submodules, using: SubmoduleEntity
expose :parent_tree_url do |tree|
path = tree.path.sub(%r{\A/}, '')
next unless path.present?
path_segments = path.split('/')
path_segments.pop
parent_tree_path = path_segments.join('/')
project_tree_path(request.project, File.join(request.ref, parent_tree_path))
end
end end
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo? - if show_new_repo?
= icon('long-arrow-right', title: 'to target branch') .tree-ref-target-holder.js-tree-ref-target-holder
= render 'shared/target_switcher', destination: 'tree', path: @path = icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path
- unless show_new_repo? - unless show_new_repo?
= render 'projects/tree/old_tree_header' = render 'projects/tree/old_tree_header'
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- @options && @options.each do |key, value| - @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Switch branch/tag") = dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags") = dropdown_filter _("Search branches and tags")
......
- dropdown_toggle_text = @ref || @project.default_branch - dropdown_toggle_text = @ref || @project.default_branch
= form_tag nil, method: :get, class: "project-refs-target-form" do = form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
- if defined?(path) - if defined?(path)
= hidden_field_tag :path, path = hidden_field_tag :path, path
......
#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } } #repo{ data: { url: content_url,
%repo project_name: project.name,
refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
import Vue from 'vue'; import Vue from 'vue';
import repoCommitSection from '~/repo/components/repo_commit_section.vue'; import repoCommitSection from '~/repo/components/repo_commit_section.vue';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
import RepoHelper from '~/repo/helpers/repo_helper';
import Api from '~/api'; import Api from '~/api';
describe('RepoCommitSection', () => { describe('RepoCommitSection', () => {
const branch = 'master'; const branch = 'master';
const projectUrl = 'projectUrl'; const projectUrl = 'projectUrl';
const openedFiles = [{ const changedFiles = [{
id: 0, id: 0,
changed: true, changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
path: 'dir/file0.ext',
newContent: 'a', newContent: 'a',
}, { }, {
id: 1, id: 1,
changed: true, changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`, url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
path: 'dir/file1.ext',
newContent: 'b', newContent: 'b',
}, { }];
const openedFiles = changedFiles.concat([{
id: 2, id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext',
changed: false, changed: false,
}]; }]);
RepoStore.projectUrl = projectUrl; RepoStore.projectUrl = projectUrl;
function createComponent() { function createComponent(el) {
const RepoCommitSection = Vue.extend(repoCommitSection); const RepoCommitSection = Vue.extend(repoCommitSection);
return new RepoCommitSection().$mount(); return new RepoCommitSection().$mount(el);
} }
it('renders a commit section', () => { it('renders a commit section', () => {
RepoStore.isCommitable = true; RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch; RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles; RepoStore.openedFiles = openedFiles;
spyOn(RepoHelper, 'getBranch').and.returnValue(branch);
const vm = createComponent(); const vm = createComponent();
const changedFiles = [...vm.$el.querySelectorAll('.changed-files > li')]; const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')];
const commitMessage = vm.$el.querySelector('#commit-message'); const commitMessage = vm.$el.querySelector('#commit-message');
const submitCommit = vm.$el.querySelector('.submit-commit'); const submitCommit = vm.$refs.submitCommit;
const targetBranch = vm.$el.querySelector('.target-branch'); const targetBranch = vm.$el.querySelector('.target-branch');
expect(vm.$el.querySelector(':scope > form')).toBeTruthy(); expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
expect(vm.$el.querySelector('.staged-files').textContent).toEqual('Staged files (2)'); expect(vm.$el.querySelector('.staged-files').textContent.trim()).toEqual('Staged files (2)');
expect(changedFiles.length).toEqual(2); expect(changedFileElements.length).toEqual(2);
changedFiles.forEach((changedFile, i) => { changedFileElements.forEach((changedFile, i) => {
const filePath = RepoHelper.getFilePathFromFullPath(openedFiles[i].url, branch); expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path);
expect(changedFile.textContent).toEqual(filePath);
}); });
expect(commitMessage.tagName).toEqual('TEXTAREA'); expect(commitMessage.tagName).toEqual('TEXTAREA');
...@@ -59,9 +59,9 @@ describe('RepoCommitSection', () => { ...@@ -59,9 +59,9 @@ describe('RepoCommitSection', () => {
expect(submitCommit.type).toEqual('submit'); expect(submitCommit.type).toEqual('submit');
expect(submitCommit.disabled).toBeTruthy(); expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy(); expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
expect(vm.$el.querySelector('.commit-summary').textContent).toEqual('Commit 2 files'); expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files');
expect(targetBranch.querySelector(':scope > label').textContent).toEqual('Target branch'); expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch');
expect(targetBranch.querySelector('.help-block').textContent).toEqual(branch); expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch);
}); });
it('does not render if not isCommitable', () => { it('does not render if not isCommitable', () => {
...@@ -89,14 +89,20 @@ describe('RepoCommitSection', () => { ...@@ -89,14 +89,20 @@ describe('RepoCommitSection', () => {
const projectId = 'projectId'; const projectId = 'projectId';
const commitMessage = 'commitMessage'; const commitMessage = 'commitMessage';
RepoStore.isCommitable = true; RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles; RepoStore.openedFiles = openedFiles;
RepoStore.projectId = projectId; RepoStore.projectId = projectId;
spyOn(RepoHelper, 'getBranch').and.returnValue(branch); // We need to append to body to get form `submit` events working
// Otherwise we run into, "Form submission canceled because the form is not connected"
// See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
const el = document.createElement('div');
document.body.appendChild(el);
const vm = createComponent(); const vm = createComponent(el);
const commitMessageEl = vm.$el.querySelector('#commit-message'); const commitMessageEl = vm.$el.querySelector('#commit-message');
const submitCommit = vm.$el.querySelector('.submit-commit'); const submitCommit = vm.$refs.submitCommit;
vm.commitMessage = commitMessage; vm.commitMessage = commitMessage;
...@@ -124,10 +130,8 @@ describe('RepoCommitSection', () => { ...@@ -124,10 +130,8 @@ describe('RepoCommitSection', () => {
expect(actions[1].action).toEqual('update'); expect(actions[1].action).toEqual('update');
expect(actions[0].content).toEqual(openedFiles[0].newContent); expect(actions[0].content).toEqual(openedFiles[0].newContent);
expect(actions[1].content).toEqual(openedFiles[1].newContent); expect(actions[1].content).toEqual(openedFiles[1].newContent);
expect(actions[0].file_path) expect(actions[0].file_path).toEqual(openedFiles[0].path);
.toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[0].url, branch)); expect(actions[1].file_path).toEqual(openedFiles[1].path);
expect(actions[1].file_path)
.toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[1].url, branch));
done(); done();
}); });
...@@ -140,7 +144,6 @@ describe('RepoCommitSection', () => { ...@@ -140,7 +144,6 @@ describe('RepoCommitSection', () => {
const vm = { const vm = {
submitCommitsLoading: true, submitCommitsLoading: true,
changedFiles: new Array(10), changedFiles: new Array(10),
openedFiles: new Array(10),
commitMessage: 'commitMessage', commitMessage: 'commitMessage',
editMode: true, editMode: true,
}; };
...@@ -149,7 +152,6 @@ describe('RepoCommitSection', () => { ...@@ -149,7 +152,6 @@ describe('RepoCommitSection', () => {
expect(vm.submitCommitsLoading).toEqual(false); expect(vm.submitCommitsLoading).toEqual(false);
expect(vm.changedFiles).toEqual([]); expect(vm.changedFiles).toEqual([]);
expect(vm.openedFiles).toEqual([]);
expect(vm.commitMessage).toEqual(''); expect(vm.commitMessage).toEqual('');
expect(vm.editMode).toEqual(false); expect(vm.editMode).toEqual(false);
}); });
......
...@@ -12,18 +12,22 @@ describe('RepoEditButton', () => { ...@@ -12,18 +12,22 @@ describe('RepoEditButton', () => {
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 = [];
RepoStore.binary = false;
RepoStore.openedFiles = [{}, {}];
const vm = createComponent(); const vm = createComponent();
expect(vm.$el.tagName).toEqual('BUTTON'); expect(vm.$el.tagName).toEqual('BUTTON');
expect(vm.$el.textContent).toMatch('Edit'); expect(vm.$el.textContent).toMatch('Edit');
spyOn(vm, 'editClicked').and.callThrough(); spyOn(vm, 'editCancelClicked').and.callThrough();
spyOn(vm, 'toggleProjectRefsForm');
vm.$el.click(); vm.$el.click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.editClicked).toHaveBeenCalled(); expect(vm.editCancelClicked).toHaveBeenCalled();
expect(vm.toggleProjectRefsForm).toHaveBeenCalled();
expect(vm.$el.textContent).toMatch('Cancel edit'); expect(vm.$el.textContent).toMatch('Cancel edit');
done(); done();
}); });
...@@ -38,14 +42,10 @@ describe('RepoEditButton', () => { ...@@ -38,14 +42,10 @@ describe('RepoEditButton', () => {
}); });
describe('methods', () => { describe('methods', () => {
describe('editClicked', () => { describe('editCancelClicked', () => {
it('sets dialog to open when there are changedFiles', () => { it('sets dialog to open when there are changedFiles');
}); it('toggles editMode and calls toggleBlobView');
it('toggles editMode and calls toggleBlobView', () => {
});
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import repoEditor from '~/repo/components/repo_editor.vue'; import repoEditor from '~/repo/components/repo_editor.vue';
import RepoStore from '~/repo/stores/repo_store';
describe('RepoEditor', () => { describe('RepoEditor', () => {
function createComponent() { beforeEach(() => {
const RepoEditor = Vue.extend(repoEditor); const RepoEditor = Vue.extend(repoEditor);
return new RepoEditor().$mount(); this.vm = new RepoEditor().$mount();
} });
it('renders an ide container', (done) => {
this.vm.openedFiles = ['idiidid'];
this.vm.binary = false;
it('renders an ide container', () => { Vue.nextTick(() => {
const monacoInstance = jasmine.createSpyObj('monacoInstance', ['onMouseUp', 'onKeyUp', 'setModel', 'updateOptions']); expect(this.vm.shouldHideEditor).toBe(false);
const monaco = { expect(this.vm.$el.id).toEqual('ide');
editor: jasmine.createSpyObj('editor', ['create']), expect(this.vm.$el.tagName).toBe('DIV');
}; done();
RepoStore.monaco = monaco; });
});
monaco.editor.create.and.returnValue(monacoInstance); describe('when there are no open files', () => {
spyOn(repoEditor.watch, 'blobRaw'); it('does not render the ide', (done) => {
this.vm.openedFiles = [];
Vue.nextTick(() => {
expect(this.vm.shouldHideEditor).toBe(true);
expect(this.vm.$el.tagName).not.toBeDefined();
done();
});
});
});
const vm = createComponent(); describe('when open file is binary and not raw', () => {
it('does not render the IDE', (done) => {
this.vm.binary = true;
this.vm.activeFile = {
raw: false,
};
expect(vm.$el.id).toEqual('ide'); Vue.nextTick(() => {
expect(this.vm.shouldHideEditor).toBe(true);
expect(this.vm.$el.tagName).not.toBeDefined();
done();
});
});
}); });
}); });
...@@ -23,6 +23,7 @@ describe('RepoFileButtons', () => { ...@@ -23,6 +23,7 @@ describe('RepoFileButtons', () => {
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
RepoStore.activeFileLabel = activeFileLabel; RepoStore.activeFileLabel = activeFileLabel;
RepoStore.editMode = true; RepoStore.editMode = true;
RepoStore.binary = false;
const vm = createComponent(); const vm = createComponent();
const raw = vm.$el.querySelector('.raw'); const raw = vm.$el.querySelector('.raw');
...@@ -31,13 +32,13 @@ describe('RepoFileButtons', () => { ...@@ -31,13 +32,13 @@ describe('RepoFileButtons', () => {
expect(vm.$el.id).toEqual('repo-file-buttons'); expect(vm.$el.id).toEqual('repo-file-buttons');
expect(raw.href).toMatch(`/${activeFile.raw_path}`); expect(raw.href).toMatch(`/${activeFile.raw_path}`);
expect(raw.textContent).toEqual('Raw'); expect(raw.textContent.trim()).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blame_path}`); expect(blame.href).toMatch(`/${activeFile.blame_path}`);
expect(blame.textContent).toEqual('Blame'); expect(blame.textContent.trim()).toEqual('Blame');
expect(history.href).toMatch(`/${activeFile.commits_path}`); expect(history.href).toMatch(`/${activeFile.commits_path}`);
expect(history.textContent).toEqual('History'); expect(history.textContent.trim()).toEqual('History');
expect(vm.$el.querySelector('.permalink').textContent).toEqual('Permalink'); expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
expect(vm.$el.querySelector('.preview').textContent).toEqual(activeFileLabel); expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel);
}); });
it('triggers rawPreviewToggle on preview click', () => { it('triggers rawPreviewToggle on preview click', () => {
...@@ -71,12 +72,4 @@ describe('RepoFileButtons', () => { ...@@ -71,12 +72,4 @@ describe('RepoFileButtons', () => {
expect(vm.$el.querySelector('.preview')).toBeFalsy(); expect(vm.$el.querySelector('.preview')).toBeFalsy();
}); });
it('does not render if not isMini', () => {
RepoStore.openedFiles = [];
const vm = createComponent();
expect(vm.$el.innerHTML).toBeFalsy();
});
}); });
...@@ -39,9 +39,9 @@ describe('RepoFile', () => { ...@@ -39,9 +39,9 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px'); expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px');
expect(name.title).toEqual(file.url); expect(name.title).toEqual(file.url);
expect(name.href).toMatch(`/${file.url}`); expect(name.href).toMatch(`/${file.url}`);
expect(name.textContent).toEqual(file.name); expect(name.textContent.trim()).toEqual(file.name);
expect(vm.$el.querySelector('.commit-message').textContent).toBe(file.lastCommitMessage); expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage);
expect(vm.$el.querySelector('.commit-update').textContent).toBe(updated); expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated);
expect(fileIcon.classList.contains(file.icon)).toBeTruthy(); expect(fileIcon.classList.contains(file.icon)).toBeTruthy();
expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`); expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`);
}); });
......
...@@ -13,7 +13,7 @@ describe('RepoLoadingFile', () => { ...@@ -13,7 +13,7 @@ describe('RepoLoadingFile', () => {
function assertLines(lines) { function assertLines(lines) {
lines.forEach((line, n) => { lines.forEach((line, n) => {
const index = n + 1; const index = n + 1;
expect(line.classList.contains(`line-of-code-${index}`)).toBeTruthy(); expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
}); });
} }
......
import Vue from 'vue'; import Vue from 'vue';
import Helper from '~/repo/helpers/repo_helper';
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';
...@@ -13,6 +15,7 @@ describe('RepoSidebar', () => { ...@@ -13,6 +15,7 @@ describe('RepoSidebar', () => {
RepoStore.files = [{ RepoStore.files = [{
id: 0, id: 0,
}]; }];
RepoStore.openedFiles = [];
const vm = createComponent(); const vm = createComponent();
const thead = vm.$el.querySelector('thead'); const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody'); const tbody = vm.$el.querySelector('tbody');
...@@ -58,4 +61,51 @@ describe('RepoSidebar', () => { ...@@ -58,4 +61,51 @@ describe('RepoSidebar', () => {
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
}); });
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.isRoot = true;
const vm = createComponent();
vm.fileClicked(file1);
expect(Helper.getContent).toHaveBeenCalledWith(file1);
});
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;
const vm = createComponent();
vm.fileClicked(file1);
expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1);
});
});
describe('goToPreviousDirectoryClicked', () => {
it('should hide files in directory if already open', () => {
const prevUrl = 'foo/bar';
const vm = createComponent();
vm.goToPreviousDirectoryClicked(prevUrl);
expect(RepoService.url).toEqual(prevUrl);
});
});
});
}); });
...@@ -12,7 +12,6 @@ describe('RepoTab', () => { ...@@ -12,7 +12,6 @@ describe('RepoTab', () => {
it('renders a close link and a name link', () => { it('renders a close link and a name link', () => {
const tab = { const tab = {
loading: false,
url: 'url', url: 'url',
name: 'name', name: 'name',
}; };
...@@ -22,38 +21,21 @@ describe('RepoTab', () => { ...@@ -22,38 +21,21 @@ describe('RepoTab', () => {
const close = vm.$el.querySelector('.close'); const close = vm.$el.querySelector('.close');
const name = vm.$el.querySelector(`a[title="${tab.url}"]`); const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
spyOn(vm, 'xClicked'); spyOn(vm, 'closeTab');
spyOn(vm, 'tabClicked'); spyOn(vm, 'tabClicked');
expect(close.querySelector('.fa-times')).toBeTruthy(); expect(close.querySelector('.fa-times')).toBeTruthy();
expect(name.textContent).toEqual(tab.name); expect(name.textContent.trim()).toEqual(tab.name);
close.click(); close.click();
name.click(); name.click();
expect(vm.xClicked).toHaveBeenCalledWith(tab); expect(vm.closeTab).toHaveBeenCalledWith(tab);
expect(vm.tabClicked).toHaveBeenCalledWith(tab); expect(vm.tabClicked).toHaveBeenCalledWith(tab);
}); });
it('renders a spinner if tab is loading', () => {
const tab = {
loading: true,
url: 'url',
};
const vm = createComponent({
tab,
});
const close = vm.$el.querySelector('.close');
const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
expect(close).toBeFalsy();
expect(name).toBeFalsy();
expect(vm.$el.querySelector('.fa.fa-spinner.fa-spin')).toBeTruthy();
});
it('renders an fa-circle icon if tab is changed', () => { it('renders an fa-circle icon if tab is changed', () => {
const tab = { const tab = {
loading: false,
url: 'url', url: 'url',
name: 'name', name: 'name',
changed: true, changed: true,
...@@ -66,22 +48,22 @@ describe('RepoTab', () => { ...@@ -66,22 +48,22 @@ describe('RepoTab', () => {
}); });
describe('methods', () => { describe('methods', () => {
describe('xClicked', () => { describe('closeTab', () => {
const vm = jasmine.createSpyObj('vm', ['$emit']); 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 file = { changed: true };
const returnVal = repoTab.methods.xClicked.call(vm, file); const returnVal = repoTab.methods.closeTab.call(vm, file);
expect(returnVal).toBeUndefined(); expect(returnVal).toBeUndefined();
expect(vm.$emit).not.toHaveBeenCalled(); expect(vm.$emit).not.toHaveBeenCalled();
}); });
it('$emits xclicked event with file obj', () => { it('$emits tabclosed event with file obj', () => {
const file = { changed: false }; const file = { changed: false };
repoTab.methods.xClicked.call(vm, file); repoTab.methods.closeTab.call(vm, file);
expect(vm.$emit).toHaveBeenCalledWith('xclicked', file); expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file);
}); });
}); });
}); });
......
...@@ -18,44 +18,25 @@ describe('RepoTabs', () => { ...@@ -18,44 +18,25 @@ describe('RepoTabs', () => {
it('renders a list of tabs', () => { it('renders a list of tabs', () => {
RepoStore.openedFiles = openedFiles; RepoStore.openedFiles = openedFiles;
RepoStore.tabsOverflow = true;
const vm = createComponent(); const vm = createComponent();
const tabs = [...vm.$el.querySelectorAll(':scope > li')]; const tabs = [...vm.$el.querySelectorAll(':scope > li')];
expect(vm.$el.id).toEqual('tabs'); expect(vm.$el.id).toEqual('tabs');
expect(vm.$el.classList.contains('overflown')).toBeTruthy();
expect(tabs.length).toEqual(3); expect(tabs.length).toEqual(3);
expect(tabs[0].classList.contains('active')).toBeTruthy(); expect(tabs[0].classList.contains('active')).toBeTruthy();
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();
}); });
it('does not render a tabs list if not isMini', () => {
RepoStore.openedFiles = [];
const vm = createComponent();
expect(vm.$el.innerHTML).toBeFalsy();
});
it('does not apply overflown class if not tabsOverflow', () => {
RepoStore.openedFiles = openedFiles;
RepoStore.tabsOverflow = false;
const vm = createComponent();
expect(vm.$el.classList.contains('overflown')).toBeFalsy();
});
describe('methods', () => { describe('methods', () => {
describe('xClicked', () => { describe('tabClosed', () => {
it('calls removeFromOpenedFiles with file obj', () => { it('calls removeFromOpenedFiles with file obj', () => {
const file = {}; const file = {};
spyOn(RepoStore, 'removeFromOpenedFiles'); spyOn(RepoStore, 'removeFromOpenedFiles');
repoTabs.methods.xClicked(file); repoTabs.methods.tabClosed(file);
expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file); expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file);
}); });
......
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