diff options
author | Phil Hughes <me@iamphill.com> | 2017-10-26 11:14:31 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-10-26 11:14:31 +0100 |
commit | 05728e785ce7cd39c4c517aa0ea50b3bba44d537 (patch) | |
tree | faa93529ccd3b8a7e6ac386192d9a442ea55af25 | |
parent | 95e56a617500d6ef94d7e857370cda5eadde614f (diff) | |
download | gitlab-ce-05728e785ce7cd39c4c517aa0ea50b3bba44d537.tar.gz |
[WIP] Move multi-file editor store to Vuex
33 files changed, 910 insertions, 584 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 242b3e2b990..d963101028a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -16,6 +16,7 @@ const Api = { usersPath: '/api/:version/users.json', commitPath: '/api/:version/projects/:id/repository/commits', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', + createBranchPath: '/api/:version/projects/:id/repository/branches', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath) diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue index eac43e692b0..ba7090e4a9d 100644 --- a/app/assets/javascripts/repo/components/new_branch_form.vue +++ b/app/assets/javascripts/repo/components/new_branch_form.vue @@ -1,18 +1,12 @@ <script> + import { mapState, mapActions } from 'vuex'; import flash, { hideFlash } from '../../flash'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import eventHub from '../event_hub'; export default { components: { loadingIcon, }, - props: { - currentBranch: { - type: String, - required: true, - }, - }, data() { return { branchName: '', @@ -20,11 +14,17 @@ }; }, computed: { + ...mapState([ + 'currentBranch', + ]), btnDisabled() { return this.loading || this.branchName === ''; }, }, methods: { + ...mapActions([ + 'createNewBranch', + ]), toggleDropdown() { this.$dropdown.dropdown('toggle'); }, @@ -38,19 +38,21 @@ hideFlash(flashEl, false); } - eventHub.$emit('createNewBranch', this.branchName); - }, - showErrorMessage(message) { - this.loading = false; - flash(message, 'alert', this.$el); - }, - createdNewBranch(newBranchName) { - this.loading = false; - this.branchName = ''; + this.createNewBranch(this.branchName) + .then(() => { + this.loading = false; + this.branchName = ''; - if (this.dropdownText) { - this.dropdownText.textContent = newBranchName; - } + if (this.dropdownText) { + this.dropdownText.textContent = this.currentBranch; + } + + this.toggleDropdown(); + }) + .catch(res => res.json().then((data) => { + this.loading = false; + flash(data.message, 'alert', this.$el); + })); }, }, created() { @@ -59,15 +61,6 @@ // text element is outside Vue app 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> diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 788976a9804..0bc5271f95c 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -1,75 +1,42 @@ <script> +import { mapState, mapGetters } from 'vuex'; import RepoSidebar from './repo_sidebar.vue'; import RepoCommitSection from './repo_commit_section.vue'; import RepoTabs from './repo_tabs.vue'; import RepoFileButtons from './repo_file_buttons.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 eventHub from '../event_hub'; export default { - data() { - return Store; + computed: { + ...mapState([ + 'currentBlobView', + 'editMode', + ]), + ...mapGetters([ + 'isMini', + 'changedFiles', + ]), }, - mixins: [RepoMixin], components: { RepoSidebar, RepoTabs, RepoFileButtons, 'repo-editor': MonacoLoaderHelper.repoEditorLoader, RepoCommitSection, - PopupDialog, RepoPreview, }, - created() { - eventHub.$on('createNewBranch', this.createNewBranch); - }, mounted() { - Helper.getContent().catch(Helper.loadingError); - }, - 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); + window.onbeforeunload = (e) => { + const event = e || window.event; - 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); - eventHub.$emit('toggleNewBranchDropdown'); - }).catch((err) => { - eventHub.$emit('createNewBranchError', err.response.data.message); - }); - }, + // For Safari + return 'Are you sure you want to lose unsaved changes?'; + }; }, }; </script> @@ -88,15 +55,6 @@ export default { <repo-file-buttons/> </div> </div> - <repo-commit-section/> - <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" - /> + <repo-commit-section v-if="changedFiles.length" /> </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 0d6259a37a8..37fc35f35c9 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -1,141 +1,97 @@ <script> -import Flash from '../../flash'; -import Store from '../stores/repo_store'; -import RepoMixin from '../mixins/repo_mixin'; -import Service from '../services/repo_service'; +import { mapGetters, mapState, mapActions } from 'vuex'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; -import { visitUrl } from '../../lib/utils/url_utility'; +import { n__ } from '../../locale'; export default { - mixins: [RepoMixin], - - data() { - return Store; - }, - components: { PopupDialog, }, - + data() { + return { + showNewBranchDialog: false, + submitCommitsLoading: false, + startNewMR: false, + commitMessage: '', + }; + }, computed: { - showCommitable() { - return this.isCommitable && this.changedFiles.length; - }, - - branchPaths() { - return this.changedFiles.map(f => f.path); - }, - - cantCommitYet() { + ...mapState([ + 'currentBranch', + ]), + ...mapGetters([ + 'changedFiles', + ]), + commitButtonDisabled() { return !this.commitMessage || this.submitCommitsLoading; }, - - filePluralize() { - return this.changedFiles.length > 1 ? 'files' : 'file'; + commitButtonText() { + return n__('Commit %d file', 'Commit %d files', this.changedFiles.length); }, }, - methods: { - commitToNewBranch(status) { - if (status) { - this.showNewBranchDialog = false; - this.tryCommit(null, true, true); - } else { - // reset the state - } - }, + ...mapActions([ + 'checkCommitStatus', + 'commitChanges', + ]), + makeCommit(newBranch = false) { + 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 = { - branch, - commit_message: commitMessage, - actions, + branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch, + commit_message: this.commitMessage, + 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; - } - Service.commitFiles(payload) + + this.showNewBranchDialog = false; + this.submitCommitsLoading = true; + + this.commitChanges({ payload, newMr: this.startNewMR }) .then(() => { - this.resetCommitState(); - if (this.startNewMR) { - this.redirectToNewMr(branch); - } else { - this.redirectToBranch(branch); - } + this.submitCommitsLoading = false; }) .catch(() => { - Flash('An error occurred while committing your changes'); + this.submitCommitsLoading = false; }); }, - - tryCommit(e, skipBranchCheck = false, newBranch = false) { + tryCommit() { this.submitCommitsLoading = true; - if (skipBranchCheck) { - this.makeCommit(newBranch); - } else { - Store.setBranchHash() - .then(() => { - if (Store.branchChanged) { - Store.showNewBranchDialog = true; - return; - } - this.makeCommit(newBranch); - }) - .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); + this.checkCommitStatus() + .then((branchChanged) => { + if (branchChanged) { + this.showNewBranchDialog = true; + } else { + this.makeCommit(); + } + }) + .catch(() => { + this.submitCommitsLoading = false; + }); }, }, }; </script> <template> -<div - v-if="showCommitable" - id="commit-area"> +<div id="commit-area"> <popup-dialog v-if="showNewBranchDialog" :primary-button-label="__('Create new branch')" kind="primary" :title="__('Branch has changed')" :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 class="form-horizontal" - @submit.prevent="tryCommit"> + @submit.prevent="tryCommit()"> <fieldset> <div class="form-group"> <label class="col-md-4 control-label staged-files"> @@ -144,10 +100,10 @@ export default { <div class="col-md-6"> <ul class="list-unstyled changed-files"> <li - v-for="branchPath in branchPaths" - :key="branchPath"> + v-for="(file, index) in changedFiles" + :key="index"> <span class="help-block"> - {{branchPath}} + {{ file.path }} </span> </li> </ul> @@ -182,9 +138,8 @@ export default { </div> <div class="col-md-offset-4 col-md-6"> <button - ref="submitCommit" type="submit" - :disabled="cantCommitYet" + :disabled="commitButtonDisabled" class="btn btn-success"> <i v-if="submitCommitsLoading" @@ -193,7 +148,7 @@ export default { aria-label="loading"> </i> <span class="commit-summary"> - Commit {{changedFiles.length}} {{filePluralize}} + {{ commitButtonText }} </span> </button> </div> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index e6e8b2e5205..59c1ed6e4be 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -1,50 +1,57 @@ <script> -import Store from '../stores/repo_store'; -import RepoMixin from '../mixins/repo_mixin'; +import { mapGetters, mapActions, mapState } from 'vuex'; +import popupDialog from '../../vue_shared/components/popup_dialog.vue'; export default { - data() { - return Store; + components: { + popupDialog, }, - mixins: [RepoMixin], computed: { + ...mapState([ + 'editMode', + 'discardPopupOpen', + ]), + ...mapGetters([ + 'canEditFile', + ]), buttonLabel() { return this.editMode ? this.__('Cancel edit') : this.__('Edit'); }, - - showButton() { - return this.isCommitable && - !this.activeFile.render_error && - !this.binary && - this.openedFiles.length; - }, }, methods: { - editCancelClicked() { - if (this.changedFiles.length) { - this.dialog.open = true; - return; - } - this.editMode = !this.editMode; - Store.toggleBlobView(); - }, + ...mapActions([ + 'toggleEditMode', + 'closeDiscardPopup', + ]), }, }; </script> <template> -<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> + <div> + <button + v-if="canEditFile" + class="btn btn-default" + type="button" + @click.prevent="toggleEditMode()"> + <i + v-if="!editMode" + class="fa fa-pencil" + aria-hidden="true"> + </i> + <span> + {{buttonLabel}} + </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> diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index df4caba51d8..06ded6332e1 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -1,122 +1,83 @@ <script> /* global monaco */ -import Store from '../stores/repo_store'; -import Service from '../services/repo_service'; +import { mapGetters, mapActions } from 'vuex'; import Helper from '../helpers/repo_helper'; +import flash from '../../flash'; -const RepoEditor = { - data() { - return Store; - }, - +export default { destroyed() { - if (Helper.monacoInstance) { - Helper.monacoInstance.destroy(); + if (this.monacoInstance) { + this.monacoInstance.destroy(); } }, - mounted() { - Service.getRaw(this.activeFile) - .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); + this.initMonaco(); }, - 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'; + ...mapActions([ + 'getRawFileData', + 'changeFileContent', + ]), + initMonaco() { + if (this.monacoInstance) { + this.monacoInstance.setModel(null); } - }, - addMonacoEvents() { - Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); - Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); - }, + this.getRawFileData(this.activeFile) + .then(() => { + if (!this.monacoInstance) { + this.monacoInstance = Helper.monaco.editor.create(this.$el, { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + }); - onMonacoEditorKeysPressed() { - 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); - } - }, - }, + this.languages = Helper.monaco.languages.getLanguages(); - watch: { - 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; + this.addMonacoEvents(); + } - return f; - }); - this.editMode = false; - Store.toggleBlobView(); - } - }, - deep: true, + this.setupEditor(); + }) + .catch(() => flash('Error setting up monaco. Please try again.')); }, - - blobRaw() { - if (Helper.monacoInstance) { - this.setupEditor(); - } + setupEditor() { + const foundLang = this.languages.find(lang => + lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0, + ); + const newModel = Helper.monaco.editor.createModel( + this.activeFile.raw, foundLang ? foundLang.id : 'plaintext', + ); + + this.monacoInstance.setModel(newModel); }, - - activeLine() { - if (Helper.monacoInstance) { - Helper.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, + addMonacoEvents() { + this.monacoInstance.onKeyUp(() => { + this.changeFileContent({ + file: this.activeFile, + content: this.monacoInstance.getValue(), }); + }); + }, + }, + watch: { + activeFile(oldVal, newVal) { + if (newVal.active) { + this.initMonaco(); } }, }, computed: { + ...mapGetters([ + 'activeFile', + 'activeFileExtension', + ]), shouldHideEditor() { - return !this.openedFiles.length || (this.binary && !this.activeFile.raw); + return this.activeFile.binary && !this.activeFile.raw; }, }, }; - -export default RepoEditor; </script> <template> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 8c86e87ed3a..f78c9591da4 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -1,11 +1,9 @@ <script> + import { mapActions, mapGetters } from 'vuex'; import timeAgoMixin from '../../vue_shared/mixins/timeago'; - import eventHub from '../event_hub'; - import repoMixin from '../mixins/repo_mixin'; export default { mixins: [ - repoMixin, timeAgoMixin, ], props: { @@ -15,13 +13,15 @@ }, }, computed: { + ...mapGetters([ + 'isMini', + ]), fileIcon() { - const classObj = { + return { 'fa-spinner fa-spin': this.file.loading, [this.file.icon]: !this.file.loading, 'fa-folder-open': !this.file.loading && this.file.opened, }; - return classObj; }, levelIndentation() { return { @@ -33,9 +33,10 @@ }, }, methods: { - linkClicked(file) { - eventHub.$emit('fileNameClicked', file); - }, + ...mapActions([ + 'getTreeData', + 'clickedTreeRow', + ]), }, }; </script> @@ -43,7 +44,7 @@ <template> <tr class="file" - @click.prevent="linkClicked(file)"> + @click.prevent="clickedTreeRow(file)"> <td> <i class="fa fa-fw file-icon" diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index c98f641c853..3d72a635035 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -1,37 +1,22 @@ <script> -import Store from '../stores/repo_store'; -import Helper from '../helpers/repo_helper'; -import RepoMixin from '../mixins/repo_mixin'; - -const RepoFileButtons = { - data() { - return Store; - }, - - mixins: [RepoMixin], +import { mapGetters } from 'vuex'; +export default { computed: { + ...mapGetters([ + 'activeFile', + ]), showButtons() { - return this.activeFile.raw_path || - this.activeFile.blame_path || - this.activeFile.commits_path || + return this.activeFile.rawPath || + this.activeFile.blamePath || + this.activeFile.commitsPath || this.activeFile.permalink; }, rawDownloadButtonLabel() { - return this.binary ? 'Download' : 'Raw'; - }, - - canPreview() { - return Helper.isRenderable(); + return this.activeFile.binary ? 'Download' : 'Raw'; }, }, - - methods: { - rawPreviewToggle: Store.toggleRawPreview, - }, }; - -export default RepoFileButtons; </script> <template> @@ -40,11 +25,11 @@ export default RepoFileButtons; class="repo-file-buttons" > <a - :href="activeFile.raw_path" + :href="activeFile.rawPath" target="_blank" class="btn btn-default raw" rel="noopener noreferrer"> - {{rawDownloadButtonLabel}} + {{ rawDownloadButtonLabel }} </a> <div @@ -52,12 +37,12 @@ export default RepoFileButtons; role="group" aria-label="File actions"> <a - :href="activeFile.blame_path" + :href="activeFile.blamePath" class="btn btn-default blame"> Blame </a> <a - :href="activeFile.commits_path" + :href="activeFile.commitsPath" class="btn btn-default history"> History </a> @@ -68,12 +53,12 @@ export default RepoFileButtons; </a> </div> - <a + <!-- <a v-if="canPreview" href="#" @click.prevent="rawPreviewToggle" class="btn btn-default preview"> {{activeFileLabel}} - </a> + </a> --> </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index 832b45b2b29..51a2aade464 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -1,10 +1,12 @@ <script> - import repoMixin from '../mixins/repo_mixin'; + import { mapGetters } from 'vuex'; export default { - mixins: [ - repoMixin, - ], + computed: { + ...mapGetters([ + 'isMini', + ]), + }, methods: { lineOfCode(n) { return `skeleton-line-${n}`; diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index c4bf6dcdec2..3c623dd8020 100644 --- a/app/assets/javascripts/repo/components/repo_prev_directory.vue +++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue @@ -1,26 +1,22 @@ <script> - import eventHub from '../event_hub'; - import repoMixin from '../mixins/repo_mixin'; + import { mapGetters, mapState, mapActions } from 'vuex'; export default { - mixins: [ - repoMixin, - ], - props: { - prevUrl: { - type: String, - required: true, - }, - }, computed: { + ...mapState([ + 'parentTreeUrl', + ]), + ...mapGetters([ + 'isMini', + ]), colSpanCondition() { return this.isMini ? undefined : 3; }, }, methods: { - linkClicked(file) { - eventHub.$emit('goToPreviousDirectoryClicked', file); - }, + ...mapActions([ + 'getTreeData', + ]), }, }; </script> @@ -30,9 +26,9 @@ <td :colspan="colSpanCondition" class="table-cell" - @click.prevent="linkClicked(prevUrl)" + @click.prevent="getTreeData({ endpoint: parentTreeUrl })" > - <a :href="prevUrl">...</a> + <a :href="parentTreeUrl">...</a> </td> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index b5be771d539..23b71b03a14 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -1,44 +1,31 @@ <script> /* global LineHighlighter */ - -import Store from '../stores/repo_store'; +import { mapGetters } from 'vuex'; export default { - data() { - return Store; - }, computed: { - html() { - return this.activeFile.html; - }, + ...mapGetters([ + 'activeFile', + ]), }, methods: { highlightFile() { $(this.$el).find('.file-content').syntaxHighlight(); }, - highlightLine() { - if (Store.activeLine > -1) { - this.lineHighlighter.highlightHash(`#L${Store.activeLine}`); - } - }, }, mounted() { this.highlightFile(); + + // TODO: get this to work across different files this.lineHighlighter = new LineHighlighter({ fileHolderSelector: '.blob-viewer-container', scrollFileHolder: true, }); }, - watch: { - html() { - this.$nextTick(() => { - this.highlightFile(); - this.highlightLine(); - }); - }, - activeLine() { - this.highlightLine(); - }, + updated() { + this.$nextTick(() => { + this.highlightFile(); + }); }, }; </script> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 09dc9ee25d7..34eaa26cf72 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -1,109 +1,44 @@ <script> -import _ from 'underscore'; -import Service from '../services/repo_service'; -import Helper from '../helpers/repo_helper'; -import Store from '../stores/repo_store'; -import eventHub from '../event_hub'; +import { mapState, mapGetters, mapActions } from 'vuex'; import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoFile from './repo_file.vue'; import RepoLoadingFile from './repo_loading_file.vue'; -import RepoMixin from '../mixins/repo_mixin'; export default { - mixins: [RepoMixin], components: { 'repo-previous-directory': RepoPreviousDirectory, 'repo-file': RepoFile, 'repo-loading-file': RepoLoadingFile, }, created() { - window.addEventListener('popstate', this.checkHistory); + window.addEventListener('popstate', this.popHistoryState); }, destroyed() { - eventHub.$off('fileNameClicked', this.fileClicked); - eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked); - window.removeEventListener('popstate', this.checkHistory); + window.removeEventListener('popstate', this.popHistoryState); }, mounted() { - eventHub.$on('fileNameClicked', this.fileClicked); - eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked); - }, - data() { - return Store; + this.getTreeData(); }, computed: { - flattendFiles() { - const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)])); - - return _.chain(this.files) - .map(arr => [arr, mapFiles(arr)]) - .flatten() - .value(); - }, + ...mapState([ + 'loading', + 'isRoot', + ]), + ...mapState({ + projectName(state) { + return state.project.name; + }, + }), + ...mapGetters([ + 'treeList', + 'isMini', + ]), }, methods: { - checkHistory() { - let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); - if (!selectedFile) { - // 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); - }, + ...mapActions([ + 'getTreeData', + 'popHistoryState', + ]), }, }; </script> @@ -136,17 +71,16 @@ export default { </thead> <tbody> <repo-previous-directory - v-if="!isRoot && !loading.tree" - :prev-url="prevURL" + v-if="!isRoot && treeList.length" /> <repo-loading-file - v-if="!flattendFiles.length && loading.tree" + v-if="!treeList.length && loading" v-for="n in 5" :key="n" /> <repo-file - v-for="file in flattendFiles" - :key="file.id" + v-for="(file, index) in treeList" + :key="index" :file="file" /> </tbody> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 405d7b4cf86..4c418aba103 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -1,7 +1,7 @@ <script> -import Store from '../stores/repo_store'; +import { mapActions } from 'vuex'; -const RepoTab = { +export default { props: { tab: { type: Object, @@ -26,29 +26,23 @@ const RepoTab = { }, methods: { - tabClicked(file) { - Store.setActiveFiles(file); - }, - closeTab(file) { - if (file.changed || file.tempFile) return; - - Store.removeFromOpenedFiles(file); - }, + ...mapActions([ + 'setFileActive', + 'closeFile', + ]), }, }; - -export default RepoTab; </script> <template> <li :class="{ active : tab.active }" - @click="tabClicked(tab)" + @click="setFileActive(tab)" > <button type="button" class="close-btn" - @click.stop.prevent="closeTab(tab)" + @click.stop.prevent="closeFile(tab)" :aria-label="closeLabel"> <i class="fa" @@ -61,7 +55,7 @@ export default RepoTab; href="#" class="repo-tab" :title="tab.url" - @click.prevent="tabClicked(tab)"> + @click.prevent="setFileActive(tab)"> {{tab.name}} </a> </li> diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index b57cd0960de..59beae53e8d 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -1,15 +1,15 @@ <script> - import Store from '../stores/repo_store'; + import { mapState } from 'vuex'; import RepoTab from './repo_tab.vue'; - import RepoMixin from '../mixins/repo_mixin'; export default { - mixins: [RepoMixin], components: { 'repo-tab': RepoTab, }, - data() { - return Store; + computed: { + ...mapState([ + 'openFiles', + ]), }, }; </script> @@ -20,7 +20,7 @@ class="list-unstyled" > <repo-tab - v-for="tab in openedFiles" + v-for="tab in openFiles" :key="tab.id" :tab="tab" /> diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js index f8729bbf585..4d5ff92fbea 100644 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js @@ -1,18 +1,14 @@ /* 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'], () => { Helper.monaco = monaco; - Store.monacoLoading = false; resolve(RepoEditor); }, () => { - Store.monacoLoading = false; reject(); }); }); diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 72fc5a70648..475d53e5765 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -1,5 +1,5 @@ -import $ from 'jquery'; import Vue from 'vue'; +import { mapActions } from 'vuex'; import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import Service from './services/repo_service'; import Store from './stores/repo_store'; @@ -7,27 +7,11 @@ import Repo from './components/repo.vue'; import RepoEditButton from './components/repo_edit_button.vue'; import newBranchForm from './components/new_branch_form.vue'; import newDropdown from './components/new_dropdown/index.vue'; +import vStore from './stores'; 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) { Store.service = Service; - Store.service.url = data.url; Store.service.refsUrl = data.refsUrl; Store.path = data.currentPath; Store.projectId = data.projectId; @@ -47,9 +31,37 @@ function setInitialStore(data) { function initRepo(el) { return new Vue({ el, + store: vStore, components: { 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) { return createElement('repo'); }, @@ -59,6 +71,7 @@ function initRepo(el) { function initRepoEditButton(el) { return new Vue({ el, + store: vStore, components: { repoEditButton: RepoEditButton, }, @@ -87,32 +100,21 @@ function initNewBranchForm() { components: { newBranchForm, }, + store: vStore, render(createElement) { - return createElement('new-branch-form', { - props: { - currentBranch: Store.currentBranch, - }, - }); + return createElement('new-branch-form'); }, }); } -function initRepoBundle() { - const repo = document.getElementById('repo'); - const editButton = document.querySelector('.editable-mode'); - const newDropdownHolder = document.querySelector('.js-new-dropdown'); - setInitialStore(repo.dataset); - addEventsForNonVueEls(); - initDropdowns(); - - Vue.use(Translate); - - initRepo(repo); - initRepoEditButton(editButton); - initNewBranchForm(); - initNewDropdown(newDropdownHolder); -} +const repo = document.getElementById('repo'); +const editButton = document.querySelector('.editable-mode'); +const newDropdownHolder = document.querySelector('.js-new-dropdown'); +setInitialStore(repo.dataset); -$(initRepoBundle); +Vue.use(Translate); -export default initRepoBundle; +initRepo(repo); +initRepoEditButton(editButton); +initNewBranchForm(); +initNewDropdown(newDropdownHolder); diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js deleted file mode 100644 index efeda426b96..00000000000 --- a/app/assets/javascripts/repo/mixins/repo_mixin.js +++ /dev/null @@ -1,17 +0,0 @@ -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; diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js new file mode 100644 index 00000000000..57c234cd4c9 --- /dev/null +++ b/app/assets/javascripts/repo/services/index.js @@ -0,0 +1,28 @@ +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); + }, +}; diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js new file mode 100644 index 00000000000..c0b5ce47398 --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions.js @@ -0,0 +1,94 @@ +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'; diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js new file mode 100644 index 00000000000..b81a70dfd1e --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/branch.js @@ -0,0 +1,20 @@ +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); +}); diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js new file mode 100644 index 00000000000..887eecf83f8 --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/file.js @@ -0,0 +1,50 @@ +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 }); +}; diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js new file mode 100644 index 00000000000..8869bb8da2a --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/tree.js @@ -0,0 +1,70 @@ +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); + } +}; diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js new file mode 100644 index 00000000000..9fa593a82da --- /dev/null +++ b/app/assets/javascripts/repo/stores/getters.js @@ -0,0 +1,39 @@ +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); +}; diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/repo/stores/index.js new file mode 100644 index 00000000000..f8565fb244c --- /dev/null +++ b/app/assets/javascripts/repo/stores/index.js @@ -0,0 +1,15 @@ +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, +}); diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js new file mode 100644 index 00000000000..c6d0816080c --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutation_types.js @@ -0,0 +1,26 @@ +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'; diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js new file mode 100644 index 00000000000..2f9b038322b --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations.js @@ -0,0 +1,54 @@ +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, +}; diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js new file mode 100644 index 00000000000..d8229e8a620 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/branch.js @@ -0,0 +1,9 @@ +import * as types from '../mutation_types'; + +export default { + [types.SET_CURRENT_BRANCH](state, currentBranch) { + Object.assign(state, { + currentBranch, + }); + }, +}; diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/repo/stores/mutations/file.js new file mode 100644 index 00000000000..50d8a51d9b7 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/file.js @@ -0,0 +1,50 @@ +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, + }); + }, +}; diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js new file mode 100644 index 00000000000..700aff46827 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/tree.js @@ -0,0 +1,27 @@ +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, + }); + }, +}; diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 38df1e3e0d2..b68e2289319 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -7,15 +7,12 @@ const RepoStore = { canCommit: false, onTopOfBranch: false, editMode: false, - isRoot: null, - isInitialRoot: null, prevURL: '', projectId: '', projectName: '', projectUrl: '', branchUrl: '', blobRaw: '', - currentBlobView: 'repo-preview', openedFiles: [], submitCommitsLoading: false, dialog: { @@ -40,10 +37,6 @@ const RepoStore = { branchChanged: false, commitMessage: '', path: '', - loading: { - tree: false, - blob: false, - }, setBranchHash() { return Service.getBranch() diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js new file mode 100644 index 00000000000..6cf7565c955 --- /dev/null +++ b/app/assets/javascripts/repo/stores/state.js @@ -0,0 +1,21 @@ +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: '', +}; diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js new file mode 100644 index 00000000000..ec733200552 --- /dev/null +++ b/app/assets/javascripts/repo/stores/utils.js @@ -0,0 +1,73 @@ +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); +}; diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 7861f92b33f..8057d5295b9 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,11 +1,13 @@ #repo{ data: { root: @path.empty?.to_s, + root_url: project_tree_path(@project), url: content_url, + ref: @commit.id, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, 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, on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s, current_path: @path } } |