Commit 86817780 authored by Fatih Acet's avatar Fatih Acet Committed by Jarka Kadlecova

Merge branch 'repo-fixes' into 'master'

Many Repo Fixes

See merge request !13432
parent 695caab0
...@@ -97,7 +97,6 @@ const Api = { ...@@ -97,7 +97,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({
......
...@@ -126,7 +126,7 @@ import Cookies from 'js-cookie'; ...@@ -126,7 +126,7 @@ 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) {
......
...@@ -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,7 +2,6 @@ ...@@ -2,7 +2,6 @@
/* 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';
export default { export default {
...@@ -11,9 +10,12 @@ export default { ...@@ -11,9 +10,12 @@ export default {
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 @@ export default { ...@@ -28,11 +30,10 @@ export default {
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,49 +48,80 @@ export default { ...@@ -47,49 +48,80 @@ export default {
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);
}, },
}, },
}; };
</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;
...@@ -24,15 +27,11 @@ export default { ...@@ -24,15 +27,11 @@ export default {
Store.toggleBlobView(); Store.toggleBlobView();
}, },
toggleProjectRefsForm() { toggleProjectRefsForm() {
if (this.editMode) { $('.project-refs-form').toggleClass('disabled', this.editMode);
$('.project-refs-form').addClass('disabled-content'); $('.js-tree-ref-target-holder').toggle(this.editMode);
$('.project-refs-target-form').show();
} else {
$('.project-refs-form').removeClass('disabled-content');
$('.project-refs-target-form').hide();
}
}, },
}, },
watch: { watch: {
editMode() { editMode() {
this.toggleProjectRefsForm(); this.toggleProjectRefsForm();
...@@ -42,8 +41,18 @@ export default { ...@@ -42,8 +41,18 @@ export default {
</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,68 +8,78 @@ const RepoEditor = { ...@@ -8,68 +8,78 @@ 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); })
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); .catch(Helper.loadingError);
this.monacoInstance.setModel(newModel);
}).catch(Helper.loadingError);
}, },
methods: { methods: {
setupEditor() {
this.showHide();
Helper.setMonacoModelFromLanguage();
},
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
},
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,
});
},
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;
...@@ -80,21 +90,16 @@ const RepoEditor = { ...@@ -80,21 +90,16 @@ const RepoEditor = {
return f; return f;
}); });
this.editMode = false; this.editMode = false;
Store.toggleBlobView();
} }
}, },
deep: true, deep: true,
}, },
blobRaw() { blobRaw() {
if (this.isTree) return; if (Helper.monacoInstance && !this.isTree) {
this.setupEditor();
this.monacoInstance.setModel(null); }
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}, },
}, },
computed: { computed: {
......
...@@ -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>
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
fileClicked(clickedFile) { fileClicked(clickedFile) {
let file = clickedFile; let file = clickedFile;
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);
......
...@@ -18,8 +18,8 @@ const RepoTab = { ...@@ -18,8 +18,8 @@ const RepoTab = {
}, },
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;
}, },
...@@ -39,11 +39,11 @@ export default RepoTab; ...@@ -39,11 +39,11 @@ export default RepoTab;
</script> </script>
<template> <template>
<li> <li @click="tabClicked(tab)">
<a <a
href="#0" href="#0"
class="close" class="close"
@click.prevent="closeTab(tab)" @click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel"> :aria-label="closeLabel">
<i <i
class="fa" class="fa"
......
...@@ -23,8 +23,7 @@ export default RepoTabs; ...@@ -23,8 +23,7 @@ export default RepoTabs;
</script> </script>
<template> <template>
<ul id="tabs" <ul id="tabs">
v-if="isMini">
<repo-tab <repo-tab
v-for="tab in openedFiles" v-for="tab in openedFiles"
:key="tab.id" :key="tab.id"
......
/* 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);
}, () => { }, () => {
......
...@@ -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,
...@@ -37,10 +39,6 @@ const RepoHelper = { ...@@ -37,10 +39,6 @@ const RepoHelper = {
return fileName.split('.').pop(); return fileName.split('.').pop();
}, },
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
},
getLanguageIDForFile(file, langs) { getLanguageIDForFile(file, langs) {
const ext = RepoHelper.getFileExtension(file.name); const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs); const foundLang = RepoHelper.findLanguage(ext, langs);
...@@ -48,8 +46,12 @@ const RepoHelper = { ...@@ -48,8 +46,12 @@ const RepoHelper = {
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) {
...@@ -66,7 +68,7 @@ const RepoHelper = { ...@@ -66,7 +68,7 @@ const RepoHelper = {
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;
}, },
...@@ -80,22 +82,8 @@ const RepoHelper = { ...@@ -80,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;
...@@ -104,6 +92,9 @@ const RepoHelper = { ...@@ -104,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;
...@@ -141,11 +132,9 @@ const RepoHelper = { ...@@ -141,11 +132,9 @@ const RepoHelper = {
getContent(treeOrFile) { 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);
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
...@@ -246,36 +235,18 @@ const RepoHelper = { ...@@ -246,36 +235,18 @@ const RepoHelper = {
}, },
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() {
return RepoHelper.key;
},
setStateKey(key) {
RepoHelper.key = key;
},
updateHistoryEntry(url, title) { updateHistoryEntry(url, title) {
const history = window.history; const history = window.history;
...@@ -293,7 +264,7 @@ const RepoHelper = { ...@@ -293,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.');
}, },
}; };
......
...@@ -33,6 +33,8 @@ function setInitialStore(data) { ...@@ -33,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();
} }
......
...@@ -13,16 +13,6 @@ const RepoService = { ...@@ -13,16 +13,6 @@ 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 // Stop Axios from parsing a JSON file into a JS object
...@@ -77,7 +67,11 @@ const RepoService = { ...@@ -77,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();
}); });
}, },
......
...@@ -5,8 +5,9 @@ import Service from '../services/repo_service'; ...@@ -5,8 +5,9 @@ import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
monaco: {}, monaco: {},
monacoLoading: false, monacoLoading: false,
monacoInstance: {},
service: '', service: '',
canCommit: false,
onTopOfBranch: false,
editMode: false, editMode: false,
isTree: false, isTree: false,
isRoot: false, isRoot: false,
...@@ -52,14 +53,7 @@ const RepoStore = { ...@@ -52,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) {
...@@ -117,15 +111,15 @@ const RepoStore = { ...@@ -117,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;
...@@ -142,8 +136,8 @@ const RepoStore = { ...@@ -142,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.
...@@ -157,36 +151,16 @@ const RepoStore = { ...@@ -157,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;
......
...@@ -3,10 +3,6 @@ export default { ...@@ -3,10 +3,6 @@ export default {
name: 'popup-dialog', name: 'popup-dialog',
props: { props: {
open: {
type: Boolean,
required: true,
},
title: { title: {
type: String, type: String,
required: true, required: true,
...@@ -53,7 +49,6 @@ export default { ...@@ -53,7 +49,6 @@ export default {
<template> <template>
<div <div
class="modal popup-dialog" class="modal popup-dialog"
v-if="open"
role="dialog" role="dialog"
tabindex="-1"> tabindex="-1">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
......
...@@ -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 {
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
} }
.blob-viewer-container { .blob-viewer-container {
height: calc(100vh - 63px); height: calc(100vh - 62px);
overflow: auto; overflow: auto;
} }
...@@ -109,6 +109,7 @@ ...@@ -109,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;
...@@ -131,6 +132,7 @@ ...@@ -131,6 +132,7 @@
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;
...@@ -140,15 +142,15 @@ ...@@ -140,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;
} }
...@@ -198,7 +200,7 @@ ...@@ -198,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;
} }
...@@ -226,6 +228,7 @@ ...@@ -226,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;
} }
...@@ -255,7 +258,6 @@ ...@@ -255,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;
...@@ -264,7 +266,7 @@ ...@@ -264,7 +266,7 @@
} }
} }
.fa { .file-icon {
margin-right: 5px; margin-right: 5px;
} }
...@@ -274,118 +276,22 @@ ...@@ -274,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: 12px;
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);
......
...@@ -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
...@@ -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,
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,19 +12,21 @@ describe('RepoEditButton', () => { ...@@ -12,19 +12,21 @@ 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'); 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.toggleProjectRefsForm).toHaveBeenCalled();
expect(vm.$el.textContent).toMatch('Cancel edit'); expect(vm.$el.textContent).toMatch('Cancel edit');
done(); done();
...@@ -40,14 +42,10 @@ describe('RepoEditButton', () => { ...@@ -40,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', () => {
});
}); });
}); });
}); });
...@@ -32,13 +32,13 @@ describe('RepoFileButtons', () => { ...@@ -32,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', () => {
...@@ -72,12 +72,4 @@ describe('RepoFileButtons', () => { ...@@ -72,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();
}); });
} }
......
...@@ -15,6 +15,7 @@ describe('RepoSidebar', () => { ...@@ -15,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');
......
...@@ -29,14 +29,6 @@ describe('RepoTabs', () => { ...@@ -29,14 +29,6 @@ describe('RepoTabs', () => {
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();
});
describe('methods', () => { describe('methods', () => {
describe('tabClosed', () => { describe('tabClosed', () => {
it('calls removeFromOpenedFiles with file obj', () => { it('calls removeFromOpenedFiles with file obj', () => {
......
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