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