Commit 05728e78 authored by Phil Hughes's avatar Phil Hughes

[WIP] Move multi-file editor store to Vuex

parent 95e56a61
...@@ -16,6 +16,7 @@ const Api = { ...@@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath)
......
<script> <script>
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash'; import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
}, },
props: {
currentBranch: {
type: String,
required: true,
},
},
data() { data() {
return { return {
branchName: '', branchName: '',
...@@ -20,11 +14,17 @@ ...@@ -20,11 +14,17 @@
}; };
}, },
computed: { computed: {
...mapState([
'currentBranch',
]),
btnDisabled() { btnDisabled() {
return this.loading || this.branchName === ''; return this.loading || this.branchName === '';
}, },
}, },
methods: { methods: {
...mapActions([
'createNewBranch',
]),
toggleDropdown() { toggleDropdown() {
this.$dropdown.dropdown('toggle'); this.$dropdown.dropdown('toggle');
}, },
...@@ -38,19 +38,21 @@ ...@@ -38,19 +38,21 @@
hideFlash(flashEl, false); hideFlash(flashEl, false);
} }
eventHub.$emit('createNewBranch', this.branchName); this.createNewBranch(this.branchName)
}, .then(() => {
showErrorMessage(message) { this.loading = false;
this.loading = false; this.branchName = '';
flash(message, 'alert', this.$el);
},
createdNewBranch(newBranchName) {
this.loading = false;
this.branchName = '';
if (this.dropdownText) { if (this.dropdownText) {
this.dropdownText.textContent = newBranchName; this.dropdownText.textContent = this.currentBranch;
} }
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
}, },
}, },
created() { created() {
...@@ -59,15 +61,6 @@ ...@@ -59,15 +61,6 @@
// text element is outside Vue app // text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
eventHub.$on('createNewBranchError', this.showErrorMessage);
eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
},
destroyed() {
eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
eventHub.$off('createNewBranchError', this.showErrorMessage);
}, },
}; };
</script> </script>
......
<script> <script>
import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue'; import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue'; import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue'; import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue'; import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue'; import RepoPreview from './repo_preview.vue';
import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import eventHub from '../event_hub';
export default { export default {
data() { computed: {
return Store; ...mapState([
'currentBlobView',
'editMode',
]),
...mapGetters([
'isMini',
'changedFiles',
]),
}, },
mixins: [RepoMixin],
components: { components: {
RepoSidebar, RepoSidebar,
RepoTabs, RepoTabs,
RepoFileButtons, RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader, 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
RepoCommitSection, RepoCommitSection,
PopupDialog,
RepoPreview, RepoPreview,
}, },
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() { mounted() {
Helper.getContent().catch(Helper.loadingError); window.onbeforeunload = (e) => {
}, const event = e || window.event;
destroyed() {
eventHub.$off('createNewBranch', this.createNewBranch);
},
methods: {
getCurrentLocation() {
return location.href;
},
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.toggleDialogOpen(false);
this.dialog.status = status;
// remove tmp files
Helper.removeAllTmpFiles('openedFiles');
Helper.removeAllTmpFiles('files');
},
toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) {
Service.createBranch({
branch,
ref: Store.currentBranch,
}).then((res) => {
const newBranchName = res.data.name;
const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
Store.currentBranch = newBranchName; if (!this.changedFiles.length) return undefined;
history.pushState({ key: Helper.key }, '', newUrl); if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
eventHub.$emit('createNewBranchSuccess', newBranchName); // For Safari
eventHub.$emit('toggleNewBranchDropdown'); return 'Are you sure you want to lose unsaved changes?';
}).catch((err) => { };
eventHub.$emit('createNewBranchError', err.response.data.message);
});
},
}, },
}; };
</script> </script>
...@@ -88,15 +55,6 @@ export default { ...@@ -88,15 +55,6 @@ export default {
<repo-file-buttons/> <repo-file-buttons/>
</div> </div>
</div> </div>
<repo-commit-section/> <repo-commit-section v-if="changedFiles.length" />
<popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
</div> </div>
</template> </template>
<script> <script>
import Flash from '../../flash'; import { mapGetters, mapState, mapActions } from 'vuex';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility'; import { n__ } from '../../locale';
export default { export default {
mixins: [RepoMixin],
data() {
return Store;
},
components: { components: {
PopupDialog, PopupDialog,
}, },
data() {
return {
showNewBranchDialog: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: { computed: {
showCommitable() { ...mapState([
return this.isCommitable && this.changedFiles.length; 'currentBranch',
}, ]),
...mapGetters([
branchPaths() { 'changedFiles',
return this.changedFiles.map(f => f.path); ]),
}, commitButtonDisabled() {
cantCommitYet() {
return !this.commitMessage || this.submitCommitsLoading; return !this.commitMessage || this.submitCommitsLoading;
}, },
commitButtonText() {
filePluralize() { return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
return this.changedFiles.length > 1 ? 'files' : 'file';
}, },
}, },
methods: { methods: {
commitToNewBranch(status) { ...mapActions([
if (status) { 'checkCommitStatus',
this.showNewBranchDialog = false; 'commitChanges',
this.tryCommit(null, true, true); ]),
} else { makeCommit(newBranch = false) {
// reset the state const createNewBranch = newBranch || this.startNewMR;
}
},
makeCommit(newBranch) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.newContent,
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = { const payload = {
branch, branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
commit_message: commitMessage, commit_message: this.commitMessage,
actions, actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
})),
start_branch: createNewBranch ? this.currentBranch : undefined,
}; };
if (newBranch) {
payload.start_branch = this.currentBranch; this.showNewBranchDialog = false;
} this.submitCommitsLoading = true;
Service.commitFiles(payload)
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.resetCommitState(); this.submitCommitsLoading = false;
if (this.startNewMR) {
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
}) })
.catch(() => { .catch(() => {
Flash('An error occurred while committing your changes'); this.submitCommitsLoading = false;
}); });
}, },
tryCommit() {
tryCommit(e, skipBranchCheck = false, newBranch = false) {
this.submitCommitsLoading = true; this.submitCommitsLoading = true;
if (skipBranchCheck) { this.checkCommitStatus()
this.makeCommit(newBranch); .then((branchChanged) => {
} else { if (branchChanged) {
Store.setBranchHash() this.showNewBranchDialog = true;
.then(() => { } else {
if (Store.branchChanged) { this.makeCommit();
Store.showNewBranchDialog = true; }
return; })
} .catch(() => {
this.makeCommit(newBranch); this.submitCommitsLoading = false;
}) });
.catch(() => {
this.submitCommitsLoading = false;
Flash('An error occurred while committing your changes');
});
}
},
redirectToNewMr(branch) {
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
},
redirectToBranch(branch) {
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
},
resetCommitState() {
this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = [];
this.commitMessage = '';
this.editMode = false;
window.scrollTo(0, 0);
}, },
}, },
}; };
</script> </script>
<template> <template>
<div <div id="commit-area">
v-if="showCommitable"
id="commit-area">
<popup-dialog <popup-dialog
v-if="showNewBranchDialog" v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
kind="primary" kind="primary"
:title="__('Branch has changed')" :title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')" :text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@submit="commitToNewBranch" @toggle="showNewBranchDialog = false"
@submit="makeCommit(true)"
/> />
<form <form
class="form-horizontal" class="form-horizontal"
@submit.prevent="tryCommit"> @submit.prevent="tryCommit()">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files"> <label class="col-md-4 control-label staged-files">
...@@ -144,10 +100,10 @@ export default { ...@@ -144,10 +100,10 @@ export default {
<div class="col-md-6"> <div class="col-md-6">
<ul class="list-unstyled changed-files"> <ul class="list-unstyled changed-files">
<li <li
v-for="branchPath in branchPaths" v-for="(file, index) in changedFiles"
:key="branchPath"> :key="index">
<span class="help-block"> <span class="help-block">
{{branchPath}} {{ file.path }}
</span> </span>
</li> </li>
</ul> </ul>
...@@ -182,9 +138,8 @@ export default { ...@@ -182,9 +138,8 @@ export default {
</div> </div>
<div class="col-md-offset-4 col-md-6"> <div class="col-md-offset-4 col-md-6">
<button <button
ref="submitCommit"
type="submit" type="submit"
:disabled="cantCommitYet" :disabled="commitButtonDisabled"
class="btn btn-success"> class="btn btn-success">
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
...@@ -193,7 +148,7 @@ export default { ...@@ -193,7 +148,7 @@ export default {
aria-label="loading"> aria-label="loading">
</i> </i>
<span class="commit-summary"> <span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}} {{ commitButtonText }}
</span> </span>
</button> </button>
</div> </div>
......
<script> <script>
import Store from '../stores/repo_store'; import { mapGetters, mapActions, mapState } from 'vuex';
import RepoMixin from '../mixins/repo_mixin'; import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default { export default {
data() { components: {
return Store; popupDialog,
}, },
mixins: [RepoMixin],
computed: { computed: {
...mapState([
'editMode',
'discardPopupOpen',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() { buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit'); return this.editMode ? this.__('Cancel edit') : this.__('Edit');
}, },
showButton() {
return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
},
}, },
methods: { methods: {
editCancelClicked() { ...mapActions([
if (this.changedFiles.length) { 'toggleEditMode',
this.dialog.open = true; 'closeDiscardPopup',
return; ]),
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
}, },
}; };
</script> </script>
<template> <template>
<button <div>
v-if="showButton" <button
class="btn btn-default" v-if="canEditFile"
type="button" class="btn btn-default"
@click.prevent="editCancelClicked"> type="button"
<i @click.prevent="toggleEditMode()">
v-if="!editMode" <i
class="fa fa-pencil" v-if="!editMode"
aria-hidden="true"> class="fa fa-pencil"
</i> aria-hidden="true">
<span> </i>
{{buttonLabel}} <span>
</span> {{buttonLabel}}
</button> </span>
</button>
<popup-dialog
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="closeDiscardPopup"
@submit="toggleEditMode(true)"
/>
</div>
</template> </template>
<script> <script>
/* global monaco */ /* global monaco */
import Store from '../stores/repo_store'; import { mapGetters, mapActions } from 'vuex';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
import flash from '../../flash';
const RepoEditor = { export default {
data() {
return Store;
},
destroyed() { destroyed() {
if (Helper.monacoInstance) { if (this.monacoInstance) {
Helper.monacoInstance.destroy(); this.monacoInstance.destroy();
} }
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile) this.initMonaco();
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data;
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
Helper.monacoInstance = monacoInstance;
this.addMonacoEvents();
this.setupEditor();
})
.catch(Helper.loadingError);
}, },
methods: { methods: {
setupEditor() { ...mapActions([
this.showHide(); 'getRawFileData',
'changeFileContent',
Helper.setMonacoModelFromLanguage(); ]),
}, initMonaco() {
if (this.monacoInstance) {
showHide() { this.monacoInstance.setModel(null);
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
} }
},
addMonacoEvents() { this.getRawFileData(this.activeFile)
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); .then(() => {
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); if (!this.monacoInstance) {
}, this.monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
onMonacoEditorKeysPressed() { this.languages = Helper.monaco.languages.getLanguages();
Store.setActiveFileContents(Helper.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.setActiveLine(lineNumber);
}
},
},
watch: { this.addMonacoEvents();
dialog: { }
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
}
f.changed = false;
delete f.newContent;
return f; this.setupEditor();
}); })
this.editMode = false; .catch(() => flash('Error setting up monaco. Please try again.'));
Store.toggleBlobView();
}
},
deep: true,
}, },
setupEditor() {
blobRaw() { const foundLang = this.languages.find(lang =>
if (Helper.monacoInstance) { lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
this.setupEditor(); );
} const newModel = Helper.monaco.editor.createModel(
this.activeFile.raw, foundLang ? foundLang.id : 'plaintext',
);
this.monacoInstance.setModel(newModel);
}, },
addMonacoEvents() {
activeLine() { this.monacoInstance.onKeyUp(() => {
if (Helper.monacoInstance) { this.changeFileContent({
Helper.monacoInstance.setPosition({ file: this.activeFile,
lineNumber: this.activeLine, content: this.monacoInstance.getValue(),
column: 1,
}); });
});
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal.active) {
this.initMonaco();
} }
}, },
}, },
computed: { computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
shouldHideEditor() { shouldHideEditor() {
return !this.openedFiles.length || (this.binary && !this.activeFile.raw); return this.activeFile.binary && !this.activeFile.raw;
}, },
}, },
}; };
export default RepoEditor;
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [ mixins: [
repoMixin,
timeAgoMixin, timeAgoMixin,
], ],
props: { props: {
...@@ -15,13 +13,15 @@ ...@@ -15,13 +13,15 @@
}, },
}, },
computed: { computed: {
...mapGetters([
'isMini',
]),
fileIcon() { fileIcon() {
const classObj = { return {
'fa-spinner fa-spin': this.file.loading, 'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading, [this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened, 'fa-folder-open': !this.file.loading && this.file.opened,
}; };
return classObj;
}, },
levelIndentation() { levelIndentation() {
return { return {
...@@ -33,9 +33,10 @@ ...@@ -33,9 +33,10 @@
}, },
}, },
methods: { methods: {
linkClicked(file) { ...mapActions([
eventHub.$emit('fileNameClicked', file); 'getTreeData',
}, 'clickedTreeRow',
]),
}, },
}; };
</script> </script>
...@@ -43,7 +44,7 @@ ...@@ -43,7 +44,7 @@
<template> <template>
<tr <tr
class="file" class="file"
@click.prevent="linkClicked(file)"> @click.prevent="clickedTreeRow(file)">
<td> <td>
<i <i
class="fa fa-fw file-icon" class="fa fa-fw file-icon"
......
<script> <script>
import Store from '../stores/repo_store'; import { mapGetters } from 'vuex';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data() {
return Store;
},
mixins: [RepoMixin],
export default {
computed: { computed: {
...mapGetters([
'activeFile',
]),
showButtons() { showButtons() {
return this.activeFile.raw_path || return this.activeFile.rawPath ||
this.activeFile.blame_path || this.activeFile.blamePath ||
this.activeFile.commits_path || this.activeFile.commitsPath ||
this.activeFile.permalink; this.activeFile.permalink;
}, },
rawDownloadButtonLabel() { rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw'; return this.activeFile.binary ? 'Download' : 'Raw';
},
canPreview() {
return Helper.isRenderable();
}, },
}, },
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
}; };
export default RepoFileButtons;
</script> </script>
<template> <template>
...@@ -40,11 +25,11 @@ export default RepoFileButtons; ...@@ -40,11 +25,11 @@ export default RepoFileButtons;
class="repo-file-buttons" class="repo-file-buttons"
> >
<a <a
:href="activeFile.raw_path" :href="activeFile.rawPath"
target="_blank" target="_blank"
class="btn btn-default raw" class="btn btn-default raw"
rel="noopener noreferrer"> rel="noopener noreferrer">
{{rawDownloadButtonLabel}} {{ rawDownloadButtonLabel }}
</a> </a>
<div <div
...@@ -52,12 +37,12 @@ export default RepoFileButtons; ...@@ -52,12 +37,12 @@ export default RepoFileButtons;
role="group" role="group"
aria-label="File actions"> aria-label="File actions">
<a <a
:href="activeFile.blame_path" :href="activeFile.blamePath"
class="btn btn-default blame"> class="btn btn-default blame">
Blame Blame
</a> </a>
<a <a
:href="activeFile.commits_path" :href="activeFile.commitsPath"
class="btn btn-default history"> class="btn btn-default history">
History History
</a> </a>
...@@ -68,12 +53,12 @@ export default RepoFileButtons; ...@@ -68,12 +53,12 @@ export default RepoFileButtons;
</a> </a>
</div> </div>
<a <!-- <a
v-if="canPreview" v-if="canPreview"
href="#" href="#"
@click.prevent="rawPreviewToggle" @click.prevent="rawPreviewToggle"
class="btn btn-default preview"> class="btn btn-default preview">
{{activeFileLabel}} {{activeFileLabel}}
</a> </a> -->
</div> </div>
</template> </template>
<script> <script>
import repoMixin from '../mixins/repo_mixin'; import { mapGetters } from 'vuex';
export default { export default {
mixins: [ computed: {
repoMixin, ...mapGetters([
], 'isMini',
]),
},
methods: { methods: {
lineOfCode(n) { lineOfCode(n) {
return `skeleton-line-${n}`; return `skeleton-line-${n}`;
......
<script> <script>
import eventHub from '../event_hub'; import { mapGetters, mapState, mapActions } from 'vuex';
import repoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [
repoMixin,
],
props: {
prevUrl: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([
'parentTreeUrl',
]),
...mapGetters([
'isMini',
]),
colSpanCondition() { colSpanCondition() {
return this.isMini ? undefined : 3; return this.isMini ? undefined : 3;
}, },
}, },
methods: { methods: {
linkClicked(file) { ...mapActions([
eventHub.$emit('goToPreviousDirectoryClicked', file); 'getTreeData',
}, ]),
}, },
}; };
</script> </script>
...@@ -30,9 +26,9 @@ ...@@ -30,9 +26,9 @@
<td <td
:colspan="colSpanCondition" :colspan="colSpanCondition"
class="table-cell" class="table-cell"
@click.prevent="linkClicked(prevUrl)" @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
> >
<a :href="prevUrl">...</a> <a :href="parentTreeUrl">...</a>
</td> </td>
</tr> </tr>
</template> </template>
<script> <script>
/* global LineHighlighter */ /* global LineHighlighter */
import { mapGetters } from 'vuex';
import Store from '../stores/repo_store';
export default { export default {
data() {
return Store;
},
computed: { computed: {
html() { ...mapGetters([
return this.activeFile.html; 'activeFile',
}, ]),
}, },
methods: { methods: {
highlightFile() { highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight(); $(this.$el).find('.file-content').syntaxHighlight();
}, },
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
}, },
mounted() { mounted() {
this.highlightFile(); this.highlightFile();
// TODO: get this to work across different files
this.lineHighlighter = new LineHighlighter({ this.lineHighlighter = new LineHighlighter({
fileHolderSelector: '.blob-viewer-container', fileHolderSelector: '.blob-viewer-container',
scrollFileHolder: true, scrollFileHolder: true,
}); });
}, },
watch: { updated() {
html() { this.$nextTick(() => {
this.$nextTick(() => { this.highlightFile();
this.highlightFile(); });
this.highlightLine();
});
},
activeLine() {
this.highlightLine();
},
}, },
}; };
</script> </script>
......
<script> <script>
import _ from 'underscore'; import { mapState, mapGetters, mapActions } from 'vuex';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [RepoMixin],
components: { components: {
'repo-previous-directory': RepoPreviousDirectory, 'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile, 'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile, 'repo-loading-file': RepoLoadingFile,
}, },
created() { created() {
window.addEventListener('popstate', this.checkHistory); window.addEventListener('popstate', this.popHistoryState);
}, },
destroyed() { destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked); window.removeEventListener('popstate', this.popHistoryState);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory);
}, },
mounted() { mounted() {
eventHub.$on('fileNameClicked', this.fileClicked); this.getTreeData();
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data() {
return Store;
}, },
computed: { computed: {
flattendFiles() { ...mapState([
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)])); 'loading',
'isRoot',
return _.chain(this.files) ]),
.map(arr => [arr, mapFiles(arr)]) ...mapState({
.flatten() projectName(state) {
.value(); return state.project.name;
}, },
}),
...mapGetters([
'treeList',
'isMini',
]),
}, },
methods: { methods: {
checkHistory() { ...mapActions([
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); 'getTreeData',
if (!selectedFile) { 'popHistoryState',
// Maybe it is not in the current tree but in the opened tabs ]),
selectedFile = Helper.getFileFromPath(location.pathname);
}
let lineNumber = null;
if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
if (selectedFile) {
if (selectedFile.url !== this.activeFile.url) {
this.fileClicked(selectedFile, lineNumber);
} else {
Store.setActiveLine(lineNumber);
}
} else {
// Not opened at all lets open new tab
this.fileClicked({
url: location.href,
}, lineNumber);
}
},
fileClicked(clickedFile, lineNumber) {
const file = clickedFile;
if (file.loading) return;
if (file.type === 'tree' && file.opened) {
Helper.setDirectoryToClosed(file);
Store.setActiveLine(lineNumber);
} else if (file.type === 'submodule') {
file.loading = true;
gl.utils.visitUrl(file.url);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else {
file.loading = true;
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
Store.setActiveLine(lineNumber);
})
.catch(Helper.loadingError);
}
}
},
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null, true)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
}, },
}; };
</script> </script>
...@@ -136,17 +71,16 @@ export default { ...@@ -136,17 +71,16 @@ export default {
</thead> </thead>
<tbody> <tbody>
<repo-previous-directory <repo-previous-directory
v-if="!isRoot && !loading.tree" v-if="!isRoot && treeList.length"
:prev-url="prevURL"
/> />
<repo-loading-file <repo-loading-file
v-if="!flattendFiles.length && loading.tree" v-if="!treeList.length && loading"
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
/> />
<repo-file <repo-file
v-for="file in flattendFiles" v-for="(file, index) in treeList"
:key="file.id" :key="index"
:file="file" :file="file"
/> />
</tbody> </tbody>
......
<script> <script>
import Store from '../stores/repo_store'; import { mapActions } from 'vuex';
const RepoTab = { export default {
props: { props: {
tab: { tab: {
type: Object, type: Object,
...@@ -26,29 +26,23 @@ const RepoTab = { ...@@ -26,29 +26,23 @@ const RepoTab = {
}, },
methods: { methods: {
tabClicked(file) { ...mapActions([
Store.setActiveFiles(file); 'setFileActive',
}, 'closeFile',
closeTab(file) { ]),
if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file);
},
}, },
}; };
export default RepoTab;
</script> </script>
<template> <template>
<li <li
:class="{ active : tab.active }" :class="{ active : tab.active }"
@click="tabClicked(tab)" @click="setFileActive(tab)"
> >
<button <button
type="button" type="button"
class="close-btn" class="close-btn"
@click.stop.prevent="closeTab(tab)" @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"> :aria-label="closeLabel">
<i <i
class="fa" class="fa"
...@@ -61,7 +55,7 @@ export default RepoTab; ...@@ -61,7 +55,7 @@ export default RepoTab;
href="#" href="#"
class="repo-tab" class="repo-tab"
:title="tab.url" :title="tab.url"
@click.prevent="tabClicked(tab)"> @click.prevent="setFileActive(tab)">
{{tab.name}} {{tab.name}}
</a> </a>
</li> </li>
......
<script> <script>
import Store from '../stores/repo_store'; import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [RepoMixin],
components: { components: {
'repo-tab': RepoTab, 'repo-tab': RepoTab,
}, },
data() { computed: {
return Store; ...mapState([
'openFiles',
]),
}, },
}; };
</script> </script>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
class="list-unstyled" class="list-unstyled"
> >
<repo-tab <repo-tab
v-for="tab in openedFiles" v-for="tab in openFiles"
:key="tab.id" :key="tab.id"
:tab="tab" :tab="tab"
/> />
......
/* global monaco */ /* global monaco */
import RepoEditor from '../components/repo_editor.vue'; import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
function repoEditorLoader() { function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
Helper.monaco = monaco; Helper.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor); resolve(RepoEditor);
}, () => { }, () => {
Store.monacoLoading = false;
reject(); reject();
}); });
}); });
......
import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Service from './services/repo_service'; import Service from './services/repo_service';
import Store from './stores/repo_store'; import Store from './stores/repo_store';
...@@ -7,27 +7,11 @@ import Repo from './components/repo.vue'; ...@@ -7,27 +7,11 @@ import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue'; import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue'; import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue'; import newDropdown from './components/new_dropdown/index.vue';
import vStore from './stores';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.js-tree-ref-target-holder').hide();
}
function addEventsForNonVueEls() {
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
if (!hasChanged) return undefined;
const event = e || window.event;
if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
// For Safari
return 'Are you sure you want to lose unsaved changes?';
};
}
function setInitialStore(data) { function setInitialStore(data) {
Store.service = Service; Store.service = Service;
Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl; Store.service.refsUrl = data.refsUrl;
Store.path = data.currentPath; Store.path = data.currentPath;
Store.projectId = data.projectId; Store.projectId = data.projectId;
...@@ -47,9 +31,37 @@ function setInitialStore(data) { ...@@ -47,9 +31,37 @@ function setInitialStore(data) {
function initRepo(el) { function initRepo(el) {
return new Vue({ return new Vue({
el, el,
store: vStore,
components: { components: {
repo: Repo, repo: Repo,
}, },
methods: {
...mapActions([
'setInitialData',
]),
},
created() {
const data = el.dataset;
this.setInitialData({
project: {
id: data.projectId,
name: data.projectName,
},
endpoints: {
rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl,
rootUrl: data.rootUrl,
},
canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
currentRef: data.ref,
// TODO: get through data attribute
currentBranch: document.querySelector('.js-project-refs-dropdown').dataset.ref,
isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root),
});
},
render(createElement) { render(createElement) {
return createElement('repo'); return createElement('repo');
}, },
...@@ -59,6 +71,7 @@ function initRepo(el) { ...@@ -59,6 +71,7 @@ function initRepo(el) {
function initRepoEditButton(el) { function initRepoEditButton(el) {
return new Vue({ return new Vue({
el, el,
store: vStore,
components: { components: {
repoEditButton: RepoEditButton, repoEditButton: RepoEditButton,
}, },
...@@ -87,32 +100,21 @@ function initNewBranchForm() { ...@@ -87,32 +100,21 @@ function initNewBranchForm() {
components: { components: {
newBranchForm, newBranchForm,
}, },
store: vStore,
render(createElement) { render(createElement) {
return createElement('new-branch-form', { return createElement('new-branch-form');
props: {
currentBranch: Store.currentBranch,
},
});
}, },
}); });
} }
function initRepoBundle() { const repo = document.getElementById('repo');
const repo = document.getElementById('repo'); const editButton = document.querySelector('.editable-mode');
const editButton = document.querySelector('.editable-mode'); const newDropdownHolder = document.querySelector('.js-new-dropdown');
const newDropdownHolder = document.querySelector('.js-new-dropdown'); setInitialStore(repo.dataset);
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
Vue.use(Translate);
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);
}
$(initRepoBundle); Vue.use(Translate);
export default initRepoBundle; initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);
import Store from '../stores/repo_store';
const RepoMixin = {
computed: {
isMini() {
return !!Store.openedFiles.length;
},
changedFiles() {
const changedFileList = this.openedFiles
.filter(file => file.changed || file.tempFile);
return changedFileList;
},
},
};
export default RepoMixin;
import Vue from 'vue';
import VueResource from 'vue-resource';
import Api from '../../api';
Vue.use(VueResource);
export default {
getTreeData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getFileData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getRawFileData(endpoint) {
return Vue.http.get(endpoint);
},
getBranchData(projectId, currentBranch) {
return Api.branchSingle(projectId, currentBranch);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
return Vue.http.post(url, payload);
},
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
};
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
import * as getters from './getters';
import { visitUrl } from '../../lib/utils/url_utility';
export const redirectToUrl = url => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, state }) => {
const changedFiles = getters.changedFiles(state);
changedFiles.forEach(file => commit(types.DISCARD_FILE_CHANGES, file));
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file));
};
export const toggleEditMode = ({ commit, state, dispatch }, force = false) => {
const changedFiles = getters.changedFiles(state);
if (changedFiles.length && !force) {
commit(types.TOGGLE_DISCARD_POPUP, true);
} else {
commit(types.TOGGLE_EDIT_MODE);
commit(types.TOGGLE_DISCARD_POPUP, false);
dispatch('toggleBlobView');
dispatch('discardAllChanges');
}
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
};
export const checkCommitStatus = ({ state }) => service.getBranchData(
state.project.id,
state.currentBranch,
)
.then((data) => {
const { id } = data.commit;
if (state.currentRef !== id) {
return true;
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =>
service.commit(state.project.id, payload)
.then((data) => {
if (!data.short_id) {
flash(data.message);
return;
}
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${payload.branch}`);
} else {
// TODO: push a new state with the branch name
commit(types.SET_COMMIT_REF, data.id);
dispatch('discardAllChanges');
dispatch('closeAllFiles');
dispatch('toggleEditMode');
}
})
.catch(() => flash('Error committing changes. Please try again.'));
export const popHistoryState = ({ state, dispatch }) => {
const treeList = getters.treeList(state);
const tree = treeList.find(file => file.url === state.previousUrl);
if (!tree) return;
if (tree.type === 'tree') {
dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
}
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/branch';
import service from '../../services';
import * as types from '../mutation_types';
import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch(
rootState.project.id,
{
branch,
ref: rootState.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(rootState.currentBranch, branchName);
pushState(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import { activeFile } from '../getters';
export const closeFile = ({ commit }, file) => {
if (file.changed || file.tempFile) return;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
};
export const setFileActive = ({ commit, state }, file) => {
const currentActiveFile = activeFile(state);
if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
}
commit(types.SET_FILE_ACTIVE, { file, active: true });
};
export const getFileData = ({ commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, file);
service.getFileData(file.url)
.then(res => res.json())
.then((data) => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.SET_PREVIEW_MODE);
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.');
});
};
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file.rawPath)
.then(res => res.text())
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.'));
export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import { pushState, setPageTitle } from '../utils';
export const getTreeData = (
{ commit, state },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {},
) => {
commit(types.TOGGLE_LOADING, tree);
service.getTreeData(endpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
commit(types.SET_DIRECTORY_DATA, { data, tree });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.TOGGLE_LOADING, tree);
pushState(endpoint);
})
.catch(() => {
flash('Error loading tree data. Please try again.');
commit(types.TOGGLE_LOADING, tree);
});
};
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) {
// send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] };
pushState(tree.parentTreeUrl);
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
commit(types.SET_DIRECTORY_DATA, { data, tree });
} else {
commit(types.SET_PREVIOUS_URL, endpoint);
dispatch('getTreeData', { endpoint, tree });
}
commit(types.TOGGLE_TREE_OPEN, tree);
};
export const clickedTreeRow = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', {
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row);
gl.utils.visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row);
} else {
dispatch('getFileData', row);
}
};
import _ from 'underscore';
export const treeList = (state) => {
const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(state.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
};
export const changedFiles = (state) => {
const files = state.openFiles;
return files.filter(file => file.changed);
};
export const activeFile = (state) => {
const openedFiles = state.openFiles;
return openedFiles.find(file => file.active);
};
export const activeFileExtension = (state) => {
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const isMini = state => !!state.openFiles.length;
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
const openedFiles = state.openFiles;
return state.canCommit &&
state.onTopOfBranch &&
openedFiles.length &&
(currentActiveFile && !currentActiveFile.render_error && !currentActiveFile.binary);
};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state,
actions,
mutations,
getters,
});
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
// Viewer mutation types
export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
import * as types from './mutation_types';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
export default {
[types.SET_INITIAL_DATA](state, data) {
Object.assign(state, data);
},
[types.SET_PREVIEW_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-preview',
});
},
[types.SET_EDIT_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-editor',
});
},
[types.TOGGLE_LOADING](state, entry) {
Object.assign(entry, {
loading: !entry.loading,
});
},
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
},
[types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
Object.assign(state, {
discardPopupOpen,
});
},
[types.SET_COMMIT_REF](state, ref) {
Object.assign(state, {
currentRef: ref,
});
},
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
[types.SET_PREVIOUS_URL](state, previousUrl) {
Object.assign(state, {
previousUrl,
});
},
...fileMutations,
...treeMutations,
...branchMutations,
};
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranch) {
Object.assign(state, {
currentBranch,
});
},
};
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
Object.assign(file, {
active,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
opened: !file.opened,
});
if (file.opened) {
state.openFiles.push(file);
} else {
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
}
},
[types.SET_FILE_DATA](state, { data, file }) {
Object.assign(file, {
blamePath: data.blame_path,
commitsPath: data.commits_path,
permalink: data.permalink,
rawPath: data.raw_path,
binary: data.binary,
html: data.html,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
Object.assign(file, {
content,
changed,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
changed: false,
});
},
};
import * as types from '../mutation_types';
import * as utils from '../utils';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
Object.assign(tree, {
opened: !tree.opened,
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
const level = tree.level !== undefined ? tree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
Object.assign(tree, {
tree: [
...data.trees.map(t => utils.decorateData(t, 'tree', parentTreeUrl, level)),
...data.submodules.map(m => utils.decorateData(m, 'submodule', parentTreeUrl, level)),
...data.blobs.map(b => utils.decorateData(b, 'blob', parentTreeUrl, level)),
],
});
},
[types.SET_PARENT_TREE_URL](state, url) {
Object.assign(state, {
parentTreeUrl: url,
});
},
};
...@@ -7,15 +7,12 @@ const RepoStore = { ...@@ -7,15 +7,12 @@ const RepoStore = {
canCommit: false, canCommit: false,
onTopOfBranch: false, onTopOfBranch: false,
editMode: false, editMode: false,
isRoot: null,
isInitialRoot: null,
prevURL: '', prevURL: '',
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
branchUrl: '', branchUrl: '',
blobRaw: '', blobRaw: '',
currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
submitCommitsLoading: false, submitCommitsLoading: false,
dialog: { dialog: {
...@@ -40,10 +37,6 @@ const RepoStore = { ...@@ -40,10 +37,6 @@ const RepoStore = {
branchChanged: false, branchChanged: false,
commitMessage: '', commitMessage: '',
path: '', path: '',
loading: {
tree: false,
blob: false,
},
setBranchHash() { setBranchHash() {
return Service.getBranch() return Service.getBranch()
......
export default {
project: {
id: 0,
name: '',
},
currentBranch: '',
endpoints: {},
isRoot: false,
isInitialRoot: false,
currentRef: '',
canCommit: false,
onTopOfBranch: false,
editMode: false,
loading: false,
currentBlobView: '',
discardPopupOpen: false,
tree: [],
openFiles: [],
parentTreeUrl: '',
previousUrl: '',
};
export const dataStructure = ({
id: '',
type: '',
name: '',
url: '',
path: '',
level: 0,
tempFile: false,
icon: '',
tree: [],
loading: false,
opened: false,
active: false,
changed: false,
lastCommit: {},
tree_url: '',
blamePath: '',
commitsPath: '',
permalink: '',
rawPath: '',
binary: false,
html: '',
raw: '',
content: '',
parentTreeUrl: '',
});
export const decorateData = (entity, type, parentTreeUrl = '', level = 0) => {
const {
id,
url,
name,
icon,
last_commit,
tree_url,
path,
tempFile,
active = false,
opened = false,
} = entity;
return {
...dataStructure,
id,
type,
name,
url,
tree_url,
path,
level,
tempFile,
icon: `fa-${icon}`,
opened,
active,
parentTreeUrl,
// eslint-disable-next-line camelcase
lastCommit: last_commit ? {
// url: `${Store.projectUrl}/commit/${last_commit.id}`,
message: last_commit.message,
updatedAt: last_commit.committed_date,
} : {},
};
};
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
export const pushState = (url) => {
history.pushState({ url }, '', url);
};
#repo{ data: { root: @path.empty?.to_s, #repo{ data: { root: @path.empty?.to_s,
root_url: project_tree_path(@project),
url: content_url, url: content_url,
ref: @commit.id,
project_name: project.name, project_name: project.name,
refs_url: refs_project_path(project, format: :json), refs_url: refs_project_path(project, format: :json),
project_url: project_path(project), project_url: project_path(project),
project_id: project.id, project_id: project.id,
blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'), blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }), new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s, can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s, on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
current_path: @path } } current_path: @path } }
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