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 = {
},
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({
......
......@@ -126,7 +126,7 @@ 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) {
......
......@@ -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,7 +2,6 @@
/* 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';
export default {
......@@ -11,9 +10,12 @@ export default {
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 @@ export default {
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,49 +48,80 @@ export default {
resetCommitState() {
this.submitCommitsLoading = false;
this.changedFiles = [];
this.openedFiles = [];
this.commitMessage = '';
this.editMode = false;
$('html, body').animate({ scrollTop: 0 }, 'fast');
window.scrollTo(0, 0);
},
},
};
</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;
......@@ -24,15 +27,11 @@ export default {
Store.toggleBlobView();
},
toggleProjectRefsForm() {
if (this.editMode) {
$('.project-refs-form').addClass('disabled-content');
$('.project-refs-target-form').show();
} else {
$('.project-refs-form').removeClass('disabled-content');
$('.project-refs-target-form').hide();
}
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
},
watch: {
editMode() {
this.toggleProjectRefsForm();
......@@ -42,8 +41,18 @@ export default {
</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,68 +8,78 @@ 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);
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';
} else {
this.$el.style.display = 'inline-block';
}
},
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,
});
},
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;
......@@ -80,21 +90,16 @@ const RepoEditor = {
return f;
});
this.editMode = false;
Store.toggleBlobView();
}
},
deep: true,
},
blobRaw() {
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: {
......
......@@ -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>
......@@ -35,7 +35,7 @@ export default {
fileClicked(clickedFile) {
let file = clickedFile;
if (file.loading) return;
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
......
......@@ -18,8 +18,8 @@ const RepoTab = {
},
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;
},
......@@ -39,11 +39,11 @@ export default RepoTab;
</script>
<template>
<li>
<li @click="tabClicked(tab)">
<a
href="#0"
class="close"
@click.prevent="closeTab(tab)"
@click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel">
<i
class="fa"
......
......@@ -23,8 +23,7 @@ export default RepoTabs;
</script>
<template>
<ul id="tabs"
v-if="isMini">
<ul id="tabs">
<repo-tab
v-for="tab in openedFiles"
:key="tab.id"
......
/* 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);
}, () => {
......
......@@ -4,6 +4,8 @@ import Store from '../stores/repo_store';
import '../../flash';
const RepoHelper = {
monacoInstance: null,
getDefaultActiveFile() {
return {
active: true,
......@@ -37,10 +39,6 @@ const RepoHelper = {
return fileName.split('.').pop();
},
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
},
getLanguageIDForFile(file, langs) {
const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs);
......@@ -48,8 +46,12 @@ const RepoHelper = {
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) {
......@@ -66,7 +68,7 @@ const RepoHelper = {
return file;
},
isKindaBinary() {
isRenderable() {
const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1;
},
......@@ -80,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;
......@@ -104,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;
......@@ -141,11 +132,9 @@ const RepoHelper = {
getContent(treeOrFile) {
let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
// RepoHelper.setLoading(false, loadingData);
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
......@@ -246,36 +235,18 @@ const RepoHelper = {
},
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;
},
updateHistoryEntry(url, title) {
const history = window.history;
......@@ -293,7 +264,7 @@ const RepoHelper = {
},
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) {
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();
}
......
......@@ -13,16 +13,6 @@ 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
......@@ -77,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();
});
},
......
......@@ -5,8 +5,9 @@ import Service from '../services/repo_service';
const RepoStore = {
monaco: {},
monacoLoading: false,
monacoInstance: {},
service: '',
canCommit: false,
onTopOfBranch: false,
editMode: false,
isTree: false,
isRoot: false,
......@@ -52,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) {
......@@ -117,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;
......@@ -142,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.
......@@ -157,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;
......
......@@ -3,10 +3,6 @@ export default {
name: 'popup-dialog',
props: {
open: {
type: Boolean,
required: true,
},
title: {
type: String,
required: true,
......@@ -53,7 +49,6 @@ export default {
<template>
<div
class="modal popup-dialog"
v-if="open"
role="dialog"
tabindex="-1">
<div class="modal-dialog" role="document">
......
......@@ -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 {
......@@ -85,7 +85,7 @@
}
.blob-viewer-container {
height: calc(100vh - 63px);
height: calc(100vh - 62px);
overflow: auto;
}
......@@ -109,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;
......@@ -131,6 +132,7 @@
width: 100px;
text-align: center;
vertical-align: middle;
text-decoration: none;
&.close {
width: auto;
......@@ -140,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;
}
......@@ -198,7 +200,7 @@
background: $gray-light;
padding: 20px;
span.help-block {
.help-block {
padding-top: 7px;
margin-top: 0;
}
......@@ -226,6 +228,7 @@
vertical-align: top;
width: 20%;
border-right: 1px solid $white-normal;
min-height: 475px;
height: calc(100vh + 20px);
overflow: auto;
}
......@@ -255,7 +258,6 @@
text-transform: uppercase;
font-weight: bold;
color: $gray-darkest;
width: 185px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -264,7 +266,7 @@
}
}
.fa {
.file-icon {
margin-right: 5px;
}
......@@ -274,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: 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 {
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);
......
......@@ -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
......@@ -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{ 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,19 +12,21 @@ 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();
......@@ -40,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');
});
});
});
......@@ -32,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', () => {
......@@ -72,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();
});
}
......
......@@ -15,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');
......
......@@ -29,14 +29,6 @@ describe('RepoTabs', () => {
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('tabClosed', () => {
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