diff options
63 files changed, 1826 insertions, 2447 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/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue index 3a331ed805f..a5ee4f71281 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/index.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue @@ -1,7 +1,5 @@ <script> - import RepoStore from '../../stores/repo_store'; - import RepoHelper from '../../helpers/repo_helper'; - import eventHub from '../../event_hub'; + import { mapState } from 'vuex'; import newModal from './modal.vue'; import upload from './upload.vue'; @@ -14,9 +12,13 @@ return { openModal: false, modalType: '', - currentPath: RepoStore.path, }; }, + computed: { + ...mapState([ + 'path', + ]), + }, methods: { createNewItem(type) { this.modalType = type; @@ -25,19 +27,6 @@ toggleModalOpen() { this.openModal = !this.openModal; }, - createNewEntryInStore(options, openEditMode = true) { - RepoHelper.createNewEntry(options, openEditMode); - - if (options.toggleModal) { - this.toggleModalOpen(); - } - }, - }, - created() { - eventHub.$on('createNewEntry', this.createNewEntryInStore); - }, - beforeDestroy() { - eventHub.$off('createNewEntry', this.createNewEntryInStore); }, }; </script> @@ -70,7 +59,7 @@ </li> <li> <upload - :current-path="currentPath" + :path="path" /> </li> <li> @@ -88,7 +77,7 @@ <new-modal v-if="openModal" :type="modalType" - :current-path="currentPath" + :path="path" @toggle="toggleModalOpen" /> </div> diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue index b49586de6bb..ac1f613bb71 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -1,34 +1,38 @@ <script> + import { mapActions } from 'vuex'; import { __ } from '../../../locale'; import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; - import eventHub from '../../event_hub'; export default { props: { - currentPath: { + type: { type: String, required: true, }, - type: { + path: { type: String, required: true, }, }, data() { return { - entryName: this.currentPath !== '' ? `${this.currentPath}/` : '', + entryName: this.path !== '' ? `${this.path}/` : '', }; }, components: { popupDialog, }, methods: { + ...mapActions([ + 'createTempEntry', + ]), createEntryInStore() { - eventHub.$emit('createNewEntry', { - name: this.entryName, + this.createTempEntry({ + name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), type: this.type, - toggleModal: true, }); + + this.toggleModalOpen(); }, toggleModalOpen() { this.$emit('toggle'); diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue index cbea9c08249..14ad32f4ae0 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue @@ -1,30 +1,31 @@ <script> - import eventHub from '../../event_hub'; + import { mapActions } from 'vuex'; export default { props: { - currentPath: { + path: { type: String, required: true, }, }, methods: { + ...mapActions([ + 'createTempEntry', + ]), createFile(target, file, isText) { const { name } = file; - const nameWithPath = `${this.currentPath !== '' ? `${this.currentPath}/` : ''}${name}`; let { result } = target; if (!isText) { result = result.split('base64,')[1]; } - eventHub.$emit('createNewEntry', { - name: nameWithPath, + this.createTempEntry({ + name, type: 'blob', content: result, - toggleModal: false, base64: !isText, - }, isText); + }); }, readFile(file) { const reader = new FileReader(); diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 788976a9804..98117802016 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -1,102 +1,59 @@ <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'; +import repoEditor from './repo_editor.vue'; export default { - data() { - return Store; + computed: { + ...mapState([ + 'currentBlobView', + ]), + ...mapGetters([ + 'isCollapsed', + 'changedFiles', + ]), }, - mixins: [RepoMixin], components: { RepoSidebar, RepoTabs, RepoFileButtons, - 'repo-editor': MonacoLoaderHelper.repoEditorLoader, + repoEditor, 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); - - Store.currentBranch = newBranchName; - - history.pushState({ key: Helper.key }, '', newUrl); + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = (e) => { + if (!this.changedFiles.length) return undefined; - eventHub.$emit('createNewBranchSuccess', newBranchName); - eventHub.$emit('toggleNewBranchDropdown'); - }).catch((err) => { - eventHub.$emit('createNewBranchError', err.response.data.message); + Object.assign(e, { + returnValue, }); - }, + return returnValue; + }; }, }; </script> <template> <div class="repository-view"> - <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}"> + <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}"> <repo-sidebar/> - <div v-if="isMini" - class="panel-right" - :class="{'edit-mode': editMode}"> + <div + v-if="isCollapsed" + class="panel-right" + > <repo-tabs/> <component :is="currentBlobView" - class="blob-viewer-container"/> + /> <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 649c69c43fd..377e3d65348 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -1,142 +1,100 @@ <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', + 'getTreeData', + ]), + 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, - encoding: f.base64 ? 'base64' : 'text', - })); - 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, + encoding: f.base64 ? 'base64' : 'text', + })), + 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; + this.getTreeData(); }) .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"> @@ -145,10 +103,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> @@ -183,9 +141,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" @@ -194,7 +151,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..6c1bb4b8566 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 class="editable-mode"> + <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..0d6729bb99b 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -1,124 +1,101 @@ <script> /* global monaco */ -import Store from '../stores/repo_store'; -import Service from '../services/repo_service'; -import Helper from '../helpers/repo_helper'; - -const RepoEditor = { - data() { - return Store; - }, +import { mapGetters, mapActions } from 'vuex'; +import flash from '../../flash'; +import monacoLoader from '../monaco_loader'; +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, - }); + if (this.monaco) { + this.initMonaco(); + } else { + monacoLoader(['vs/editor/editor.main'], () => { + this.monaco = monaco; + + this.initMonaco(); + }); + } + }, + methods: { + ...mapActions([ + 'getRawFileData', + 'changeFileContent', + ]), + initMonaco() { + if (this.monacoInstance) { + this.monacoInstance.setModel(null); + } - Helper.monacoInstance = monacoInstance; + this.getRawFileData(this.activeFile) + .then(() => { + if (!this.monacoInstance) { + this.monacoInstance = this.monaco.editor.create(this.$el, { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + }); - this.addMonacoEvents(); + this.languages = this.monaco.languages.getLanguages(); - this.setupEditor(); - }) - .catch(Helper.loadingError); - }, + this.addMonacoEvents(); + } - methods: { + this.setupEditor(); + }) + .catch(() => flash('Error setting up monaco. Please try again.')); + }, setupEditor() { - this.showHide(); + if (!this.activeFile) return; + const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw; - Helper.setMonacoModelFromLanguage(); - }, + const foundLang = this.languages.find(lang => + lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0, + ); + const newModel = this.monaco.editor.createModel( + content, foundLang ? foundLang.id : 'plaintext', + ); - showHide() { - if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { - this.$el.style.display = 'none'; - } else { - this.$el.style.display = 'inline-block'; - } + this.monacoInstance.setModel(newModel); }, - addMonacoEvents() { - Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); - Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); - }, - - 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.monacoInstance.onKeyUp(() => { + this.changeFileContent({ + file: this.activeFile, + content: this.monacoInstance.getValue(), + }); + }); }, }, - 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; - - return f; - }); - this.editMode = false; - Store.toggleBlobView(); - } - }, - deep: true, - }, - - blobRaw() { - if (Helper.monacoInstance) { - this.setupEditor(); - } - }, - - activeLine() { - if (Helper.monacoInstance) { - Helper.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, - }); + activeFile(oldVal, newVal) { + if (newVal && !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> -<div id="ide" v-if='!shouldHideEditor'></div> + <div + id="ide" + v-if='!shouldHideEditor' + class="blob-viewer-container blob-editor-container" + > + </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 8c86e87ed3a..7a23154b340 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([ + 'isCollapsed', + ]), 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,9 @@ }, }, methods: { - linkClicked(file) { - eventHub.$emit('fileNameClicked', file); - }, + ...mapActions([ + 'clickedTreeRow', + ]), }, }; </script> @@ -43,7 +43,7 @@ <template> <tr class="file" - @click.prevent="linkClicked(file)"> + @click.prevent="clickedTreeRow(file)"> <td> <i class="fa fa-fw file-icon" @@ -71,7 +71,7 @@ </template> </td> - <template v-if="!isMini"> + <template v-if="!isCollapsed"> <td class="hidden-sm hidden-xs"> <a @click.stop diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index c98f641c853..dd948ee84fb 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> @@ -67,13 +52,5 @@ export default RepoFileButtons; Permalink </a> </div> - - <a - v-if="canPreview" - href="#" - @click.prevent="rawPreviewToggle" - class="btn btn-default preview"> - {{activeFileLabel}} - </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..1e6c405f292 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([ + 'isCollapsed', + ]), + }, methods: { lineOfCode(n) { return `skeleton-line-${n}`; @@ -28,7 +30,7 @@ </div> </div> </td> - <template v-if="!isMini"> + <template v-if="!isCollapsed"> <td class="hidden-sm hidden-xs"> <div class="animation-container"> diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index c4bf6dcdec2..a2b305bbd05 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([ + 'isCollapsed', + ]), colSpanCondition() { - return this.isMini ? undefined : 3; + return this.isCollapsed ? 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 264694f01a2..d1883299bd9 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -1,26 +1,20 @@ <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', + ]), + renderErrorTooLarge() { + return this.activeFile.renderError === 'too_large'; }, }, methods: { highlightFile() { $(this.$el).find('.file-content').syntaxHighlight(); }, - highlightLine() { - if (Store.activeLine > -1) { - this.lineHighlighter.highlightHash(`#L${Store.activeLine}`); - } - }, }, mounted() { this.highlightFile(); @@ -29,24 +23,18 @@ export default { scrollFileHolder: true, }); }, - watch: { - html() { - this.$nextTick(() => { - this.highlightFile(); - this.highlightLine(); - }); - }, - activeLine() { - this.highlightLine(); - }, + updated() { + this.$nextTick(() => { + this.highlightFile(); + }); }, }; </script> <template> -<div> +<div class="blob-viewer-container"> <div - v-if="!activeFile.render_error" + v-if="!activeFile.renderError" v-html="activeFile.html"> </div> <div @@ -57,17 +45,17 @@ export default { </p> </div> <div - v-else-if="activeFile.tooLarge" + v-else-if="renderErrorTooLarge" class="vertical-center render-error"> <p class="text-center"> - The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead. + The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead. </p> </div> <div v-else class="vertical-center render-error"> <p class="text-center"> - The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead. + The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead. </p> </div> </div> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 09dc9ee25d7..63c0d70f5c0 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -1,120 +1,55 @@ <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', + 'isCollapsed', + ]), }, 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> <template> -<div id="sidebar" :class="{'sidebar-mini' : isMini}"> +<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}"> <table class="table"> <thead> <tr> <th - v-if="isMini" + v-if="isCollapsed" class="repo-file-options title" > <strong class="clgray"> @@ -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..da0714c368c 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, @@ -11,7 +11,7 @@ const RepoTab = { computed: { closeLabel() { - if (this.tab.changed) { + if (this.tab.changed || this.tab.tempFile) { return `${this.tab.name} changed`; } return `Close ${this.tab.name}`; @@ -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({ file: 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.stop="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/event_hub.js b/app/assets/javascripts/repo/event_hub.js deleted file mode 100644 index 0948c2e5352..00000000000 --- a/app/assets/javascripts/repo/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -export default new Vue(); diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js deleted file mode 100644 index f8729bbf585..00000000000 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ /dev/null @@ -1,25 +0,0 @@ -/* 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(); - }); - }); -} - -const MonacoLoaderHelper = { - repoEditorLoader, -}; - -export default MonacoLoaderHelper; diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js deleted file mode 100644 index 1c677049b31..00000000000 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ /dev/null @@ -1,338 +0,0 @@ -import Service from '../services/repo_service'; -import Store from '../stores/repo_store'; -import Flash from '../../flash'; - -const RepoHelper = { - monacoInstance: null, - - getDefaultActiveFile() { - return { - id: '', - active: true, - binary: false, - extension: '', - html: '', - mime_type: '', - name: '', - plain: '', - size: 0, - url: '', - raw: false, - newContent: '', - changed: false, - loading: false, - }; - }, - - key: '', - - Time: window.performance - && window.performance.now - ? window.performance - : Date, - - getFileExtension(fileName) { - return fileName.split('.').pop(); - }, - - getLanguageIDForFile(file, langs) { - const ext = RepoHelper.getFileExtension(file.name); - const foundLang = RepoHelper.findLanguage(ext, langs); - - return foundLang ? foundLang.id : 'plaintext'; - }, - - setMonacoModelFromLanguage() { - RepoHelper.monacoInstance.setModel(null); - const languages = RepoHelper.monaco.languages.getLanguages(); - const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages); - const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID); - RepoHelper.monacoInstance.setModel(newModel); - }, - - findLanguage(ext, langs) { - return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1); - }, - - setDirectoryOpen(tree, title) { - if (!tree) return; - - Object.assign(tree, { - opened: true, - }); - - RepoHelper.updateHistoryEntry(tree.url, title); - Store.path = tree.path; - }, - - setDirectoryToClosed(entry) { - Object.assign(entry, { - opened: false, - files: [], - }); - }, - - isRenderable() { - const okExts = ['md', 'svg']; - return okExts.indexOf(Store.activeFile.extension) > -1; - }, - - setBinaryDataAsBase64(file) { - Service.getBase64Content(file.raw_path) - .then((response) => { - Store.blobRaw = response; - file.base64 = response; // eslint-disable-line no-param-reassign - }) - .catch(RepoHelper.loadingError); - }, - - getContent(treeOrFile, emptyFiles = false) { - let file = treeOrFile; - - if (!Store.files.length) { - Store.loading.tree = true; - } - - return Service.getContent() - .then((response) => { - const data = response.data; - if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']); - if (data.path && !Store.isInitialRoot) { - Store.isRoot = data.path === '/'; - Store.isInitialRoot = Store.isRoot; - } - - if (file && file.type === 'blob') { - if (!file) file = data; - Store.binary = data.binary; - - if (data.binary) { - // file might be undefined - RepoHelper.setBinaryDataAsBase64(data); - Store.setViewToPreview(); - } else if (!Store.isPreviewView() && !data.render_error) { - Service.getRaw(data) - .then((rawResponse) => { - Store.blobRaw = rawResponse.data; - data.plain = rawResponse.data; - RepoHelper.setFile(data, file); - }).catch(RepoHelper.loadingError); - } - - if (Store.isPreviewView()) { - RepoHelper.setFile(data, file); - } - } else { - Store.loading.tree = false; - RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name); - - if (emptyFiles) { - Store.files = []; - } - - this.addToDirectory(file, data); - - Store.prevURL = Service.blobURLtoParentTree(Service.url); - } - }).catch(RepoHelper.loadingError); - }, - - addToDirectory(file, data) { - const tree = file || Store; - - // TODO: Figure out why `popstate` is being trigger in the specs - if (!tree.files) return; - - const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0)); - - tree.files = files; - }, - - setFile(data, file) { - const newFile = data; - newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh. - - if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { - newFile.tooLarge = true; - } - newFile.newContent = file.newContent ? file.newContent : ''; - - Store.addToOpenedFiles(newFile); - Store.setActiveFiles(newFile); - }, - - serializeRepoEntity(type, entity, level = 0) { - const { - id, - url, - name, - icon, - last_commit, - tree_url, - path, - tempFile, - active, - opened, - } = entity; - - return { - id, - type, - name, - url, - tree_url, - path, - level, - tempFile, - icon: `fa-${icon}`, - files: [], - loading: false, - opened, - active, - // eslint-disable-next-line camelcase - lastCommit: last_commit ? { - url: `${Store.projectUrl}/commit/${last_commit.id}`, - message: last_commit.message, - updatedAt: last_commit.committed_date, - } : {}, - }; - }, - - scrollTabsRight() { - const tabs = document.getElementById('tabs'); - if (!tabs) return; - tabs.scrollLeft = tabs.scrollWidth; - }, - - dataToListOfFiles(data, level) { - const { blobs, trees, submodules } = data; - return [ - ...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)), - ...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)), - ...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)), - ]; - }, - - genKey() { - return RepoHelper.Time.now().toFixed(3); - }, - - updateHistoryEntry(url, title) { - const history = window.history; - - RepoHelper.key = RepoHelper.genKey(); - - if (document.location.pathname !== url) { - history.pushState({ key: RepoHelper.key }, '', url); - } - - if (title) { - document.title = title; - } - }, - - findOpenedFileFromActive() { - return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id); - }, - - getFileFromPath(path) { - return Store.openedFiles.find(file => file.url === path); - }, - - loadingError() { - Flash('Unable to load this content at this time.'); - }, - openEditMode() { - Store.editMode = true; - Store.currentBlobView = 'repo-editor'; - }, - updateStorePath(path) { - Store.path = path; - }, - findOrCreateEntry(type, tree, name) { - let exists = true; - let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name); - - if (!foundEntry) { - foundEntry = RepoHelper.serializeRepoEntity(type, { - id: name, - name, - path: tree.path ? `${tree.path}/${name}` : name, - icon: type === 'tree' ? 'folder' : 'file-text-o', - tempFile: true, - opened: true, - active: true, - }, tree.level !== undefined ? tree.level + 1 : 0); - - exists = false; - tree.files.push(foundEntry); - } - - return { - entry: foundEntry, - exists, - }; - }, - removeAllTmpFiles(storeFilesKey) { - Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile); - }, - createNewEntry(options, openEditMode = true) { - const { - name, - type, - content = '', - base64 = false, - } = options; - const originalPath = Store.path; - let entryName = name; - - if (entryName.indexOf(`${originalPath}/`) !== 0) { - this.updateStorePath(''); - } else { - entryName = entryName.replace(`${originalPath}/`, ''); - } - - if (entryName === '') return; - - const fileName = type === 'tree' ? '.gitkeep' : entryName; - let tree = Store; - - if (type === 'tree') { - const dirNames = entryName.split('/'); - - dirNames.forEach((dirName) => { - if (dirName === '') return; - - tree = this.findOrCreateEntry('tree', tree, dirName).entry; - }); - } - - if ((type === 'tree' && tree.tempFile) || type === 'blob') { - const file = this.findOrCreateEntry('blob', tree, fileName); - - if (file.exists) { - Flash(`The name "${file.entry.name}" is already taken in this directory.`); - } else { - const { entry } = file; - entry.newContent = content; - entry.base64 = base64; - - if (entry.base64) { - entry.render_error = true; - } - - this.setFile(entry, entry); - - if (openEditMode) { - this.openEditMode(); - } else { - file.entry.render_error = 'asdsad'; - } - } - } - - this.updateStorePath(originalPath); - }, -}; - -export default RepoHelper; diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 72fc5a70648..b6801af7fcb 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -1,55 +1,50 @@ -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'; 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 store 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; - Store.projectName = data.projectName; - Store.projectUrl = data.projectUrl; - Store.canCommit = data.canCommit; - Store.onTopOfBranch = data.onTopOfBranch; - Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl); - Store.customBranchURL = decodeURIComponent(data.blobUrl); - Store.isRoot = convertPermissionToBoolean(data.root); - Store.isInitialRoot = convertPermissionToBoolean(data.root); - Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); - Store.checkIsCommitable(); - Store.setBranchHash(); -} - function initRepo(el) { + if (!el) return null; + return new Vue({ el, + store, components: { repo: Repo, }, + methods: { + ...mapActions([ + 'setInitialData', + ]), + }, + created() { + const data = el.dataset; + + this.setInitialData({ + project: { + id: data.projectId, + name: data.projectName, + url: data.projectUrl, + }, + endpoints: { + rootEndpoint: data.url, + newMergeRequestUrl: data.newMergeRequestUrl, + rootUrl: data.rootUrl, + }, + canCommit: convertPermissionToBoolean(data.canCommit), + onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch), + currentRef: data.ref, + path: data.currentPath, + currentBranch: data.currentBranch, + isRoot: convertPermissionToBoolean(data.root), + isInitialRoot: convertPermissionToBoolean(data.root), + }); + }, render(createElement) { return createElement('repo'); }, @@ -59,15 +54,20 @@ function initRepo(el) { function initRepoEditButton(el) { return new Vue({ el, + store, components: { repoEditButton: RepoEditButton, }, + render(createElement) { + return createElement('repo-edit-button'); + }, }); } function initNewDropdown(el) { return new Vue({ el, + store, components: { newDropdown, }, @@ -87,32 +87,20 @@ function initNewBranchForm() { components: { newBranchForm, }, + store, 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'); -$(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..dc222ccac01 --- /dev/null +++ b/app/assets/javascripts/repo/services/index.js @@ -0,0 +1,33 @@ +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(file) { + if (file.tempFile) { + return Promise.resolve(file.content); + } + + return Vue.http.get(file.rawPath, { params: { format: 'json' } }) + .then(res => res.text()); + }, + 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/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js deleted file mode 100644 index d003e2b0a5e..00000000000 --- a/app/assets/javascripts/repo/services/repo_service.js +++ /dev/null @@ -1,101 +0,0 @@ -import axios from 'axios'; -import csrf from '../../lib/utils/csrf'; -import Store from '../stores/repo_store'; -import Api from '../../api'; -import Helper from '../helpers/repo_helper'; - -axios.defaults.headers.common[csrf.headerKey] = csrf.token; - -const RepoService = { - url: '', - options: { - params: { - format: 'json', - }, - }, - createBranchPath: '/api/:version/projects/:id/repository/branches', - richExtensionRegExp: /md/, - - getRaw(file) { - if (file.tempFile) { - return Promise.resolve({ - data: file.newContent ? file.newContent : '', - }); - } - - return axios.get(file.raw_path, { - // Stop Axios from parsing a JSON file into a JS object - transformResponse: [res => res], - }); - }, - - buildParams(url = this.url) { - // shallow clone object without reference - const params = Object.assign({}, this.options.params); - - if (this.urlIsRichBlob(url)) params.viewer = 'rich'; - - return params; - }, - - urlIsRichBlob(url = this.url) { - const extension = Helper.getFileExtension(url); - - return this.richExtensionRegExp.test(extension); - }, - - getContent(url = this.url) { - const params = this.buildParams(url); - - return axios.get(url, { - params, - }); - }, - - getBase64Content(url = this.url) { - const request = axios.get(url, { - responseType: 'arraybuffer', - }); - - return request.then(response => this.bufferToBase64(response.data)); - }, - - bufferToBase64(data) { - return new Buffer(data, 'binary').toString('base64'); - }, - - blobURLtoParentTree(url) { - const urlArray = url.split('/'); - urlArray.pop(); - const blobIndex = urlArray.lastIndexOf('blob'); - - if (blobIndex > -1) urlArray[blobIndex] = 'tree'; - - return urlArray.join('/'); - }, - - getBranch() { - return Api.branchSingle(Store.projectId, Store.currentBranch); - }, - - commitFiles(payload) { - return Api.commitMultiple(Store.projectId, payload) - .then(this.commitFlash); - }, - - createBranch(payload) { - const url = Api.buildUrl(this.createBranchPath) - .replace(':id', Store.projectId); - return axios.post(url, payload); - }, - - commitFlash(data) { - if (data.short_id && data.stats) { - window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); - } else { - window.Flash(data.message); - } - }, -}; - -export default RepoService; diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js new file mode 100644 index 00000000000..ca2f2a5ce7a --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions.js @@ -0,0 +1,129 @@ +import Vue from 'vue'; +import flash from '../../flash'; +import service from '../services'; +import * as types from './mutation_types'; + +export const redirectToUrl = url => gl.utils.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, getters, dispatch }) => { + const changedFiles = getters.changedFiles; + + changedFiles.forEach((file) => { + commit(types.DISCARD_FILE_CHANGES, file); + + if (file.tempFile) { + dispatch('closeFile', { file, force: true }); + } + }); +}; + +export const closeAllFiles = ({ state, dispatch }) => { + state.openFiles.forEach(file => dispatch('closeFile', { file })); +}; + +export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => { + const changedFiles = getters.changedFiles; + + if (changedFiles.length && !force) { + commit(types.TOGGLE_DISCARD_POPUP, true); + } else { + commit(types.TOGGLE_EDIT_MODE); + commit(types.TOGGLE_DISCARD_POPUP, false); + dispatch('toggleBlobView'); + + if (!state.editMode) { + 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) => { + const { branch } = payload; + 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}${branch}`); + } else { + commit(types.SET_COMMIT_REF, data.id); + dispatch('discardAllChanges'); + dispatch('closeAllFiles'); + dispatch('toggleEditMode'); + + window.scrollTo(0, 0); + } + }) + .catch(() => flash('Error committing changes. Please try again.')); + +export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => { + if (type === 'tree') { + dispatch('createTempTree', name); + } else if (type === 'blob') { + dispatch('createTempFile', { + tree: state, + name, + base64, + content, + }); + } +}; + +export const popHistoryState = ({ state, dispatch, getters }) => { + const treeList = getters.treeList; + const tree = treeList.find(file => file.url === state.previousUrl); + + if (!tree) return; + + if (tree.type === 'tree') { + dispatch('toggleTreeOpen', { endpoint: tree.url, tree }); + } +}; + +export const scrollToTab = () => { + Vue.nextTick(() => { + const tabs = document.getElementById('tabs'); + + if (tabs) { + const tabEl = tabs.querySelector('.active .repo-tab'); + + tabEl.focus(); + } + }); +}; + +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..afbe0b78a82 --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/file.js @@ -0,0 +1,108 @@ +import { normalizeHeaders } from '../../../lib/utils/common_utils'; +import flash from '../../../flash'; +import service from '../../services'; +import * as types from '../mutation_types'; +import { + findEntry, + pushState, + setPageTitle, + createTemp, + findIndexOfFile, +} from '../utils'; + +export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => { + if ((file.changed || file.tempFile) && !force) return; + + const indexOfClosedFile = findIndexOfFile(state.openFiles, file); + const fileWasActive = file.active; + + commit(types.TOGGLE_FILE_OPEN, file); + commit(types.SET_FILE_ACTIVE, { file, active: false }); + + if (state.openFiles.length > 0 && fileWasActive) { + const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; + const nextFileToOpen = state.openFiles[nextIndexToOpen]; + + dispatch('setFileActive', nextFileToOpen); + } else if (!state.openFiles.length) { + pushState(file.parentTreeUrl); + } +}; + +export const setFileActive = ({ commit, state, getters, dispatch }, file) => { + const currentActiveFile = getters.activeFile; + + if (file.active) return; + + if (currentActiveFile) { + commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false }); + } + + commit(types.SET_FILE_ACTIVE, { file, active: true }); + dispatch('scrollToTab'); + + // reset hash for line highlighting + location.hash = ''; +}; + +export const getFileData = ({ state, commit, dispatch }, file) => { + commit(types.TOGGLE_LOADING, file); + + service.getFileData(file.url) + .then((res) => { + const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); + + setPageTitle(pageTitle); + + return res.json(); + }) + .then((data) => { + commit(types.SET_FILE_DATA, { data, file }); + commit(types.TOGGLE_FILE_OPEN, file); + dispatch('setFileActive', file); + commit(types.TOGGLE_LOADING, file); + + pushState(file.url); + }) + .catch(() => { + commit(types.TOGGLE_LOADING, file); + flash('Error loading file data. Please try again.'); + }); +}; + +export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file) + .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 }); +}; + +export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => { + const file = createTemp({ + name: name.replace(`${state.path}/`, ''), + path: tree.path, + type: 'blob', + level: tree.level !== undefined ? tree.level + 1 : 0, + changed: true, + content, + base64, + }); + + if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); + + commit(types.CREATE_TMP_FILE, { + parent: tree, + file, + }); + commit(types.TOGGLE_FILE_OPEN, file); + dispatch('setFileActive', file); + + if (!state.editMode && !file.base64) { + dispatch('toggleEditMode', true); + } + + return Promise.resolve(file); +}; 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..129743c66c2 --- /dev/null +++ b/app/assets/javascripts/repo/stores/actions/tree.js @@ -0,0 +1,110 @@ +import { normalizeHeaders } from '../../../lib/utils/common_utils'; +import flash from '../../../flash'; +import service from '../../services'; +import * as types from '../mutation_types'; +import { + pushState, + setPageTitle, + findEntry, + createTemp, +} 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); + } +}; + +export const createTempTree = ({ state, commit, dispatch }, name) => { + let tree = state; + const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/'); + + dirNames.forEach((dirName) => { + const foundEntry = findEntry(tree, 'tree', dirName); + + if (!foundEntry) { + const tmpEntry = createTemp({ + name: dirName, + path: tree.path, + type: 'tree', + level: tree.level !== undefined ? tree.level + 1 : 0, + }); + + commit(types.CREATE_TMP_TREE, { + parent: tree, + tmpEntry, + }); + commit(types.TOGGLE_TREE_OPEN, tmpEntry); + + tree = tmpEntry; + } else { + tree = foundEntry; + } + }); + + if (tree.tempFile) { + dispatch('createTempFile', { + tree, + name: '.gitkeep', + }); + } +}; diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js new file mode 100644 index 00000000000..1ed05ac6e35 --- /dev/null +++ b/app/assets/javascripts/repo/stores/getters.js @@ -0,0 +1,36 @@ +import _ from 'underscore'; + +/* + Takes the multi-dimensional tree and returns a flattened array. + This allows for the table to recursively render the table rows but keeps the data + structure nested to make it easier to add new files/directories. +*/ +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 => state.openFiles.filter(file => file.changed); + +export const activeFile = state => state.openFiles.find(file => file.active); + +export const activeFileExtension = (state) => { + const file = activeFile(state); + return file ? `.${file.path.split('.').pop()}` : ''; +}; + +export const isCollapsed = 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.renderError && !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..6ac9bfd8189 --- /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: 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..4722a7dd0df --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutation_types.js @@ -0,0 +1,28 @@ +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'; +export const CREATE_TMP_TREE = 'CREATE_TMP_TREE'; + +// 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'; +export const CREATE_TMP_FILE = 'CREATE_TMP_FILE'; + +// 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..f9ba80b9dc2 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/file.js @@ -0,0 +1,54 @@ +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, + renderError: data.render_error, + }); + }, + [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, + }); + }, + [types.CREATE_TMP_FILE](state, { file, parent }) { + parent.tree.push(file); + }, +}; 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..52be2673107 --- /dev/null +++ b/app/assets/javascripts/repo/stores/mutations/tree.js @@ -0,0 +1,45 @@ +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, + type: 'tree', + parentTreeUrl, + level, + }, state.project.url)), + ...data.submodules.map(m => utils.decorateData({ + ...m, + type: 'submodule', + parentTreeUrl, + level, + }, state.project.url)), + ...data.blobs.map(b => utils.decorateData({ + ...b, + type: 'blob', + parentTreeUrl, + level, + }, state.project.url)), + ], + }); + }, + [types.SET_PARENT_TREE_URL](state, url) { + Object.assign(state, { + parentTreeUrl: url, + }); + }, + [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) { + parent.tree.push(tmpEntry); + }, +}; diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js deleted file mode 100644 index 38df1e3e0d2..00000000000 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ /dev/null @@ -1,189 +0,0 @@ -import Helper from '../helpers/repo_helper'; -import Service from '../services/repo_service'; - -const RepoStore = { - monacoLoading: false, - service: '', - canCommit: false, - onTopOfBranch: false, - editMode: false, - isRoot: null, - isInitialRoot: null, - prevURL: '', - projectId: '', - projectName: '', - projectUrl: '', - branchUrl: '', - blobRaw: '', - currentBlobView: 'repo-preview', - openedFiles: [], - submitCommitsLoading: false, - dialog: { - open: false, - title: '', - status: false, - }, - showNewBranchDialog: false, - activeFile: Helper.getDefaultActiveFile(), - activeFileIndex: 0, - activeLine: -1, - activeFileLabel: 'Raw', - files: [], - isCommitable: false, - binary: false, - currentBranch: '', - startNewMR: false, - currentHash: '', - currentShortHash: '', - customBranchURL: '', - newMrTemplateUrl: '', - branchChanged: false, - commitMessage: '', - path: '', - loading: { - tree: false, - blob: false, - }, - - setBranchHash() { - return Service.getBranch() - .then((data) => { - if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) { - RepoStore.branchChanged = true; - } - RepoStore.currentHash = data.commit.id; - RepoStore.currentShortHash = data.commit.short_id; - }); - }, - - // mutations - checkIsCommitable() { - RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; - }, - - toggleRawPreview() { - RepoStore.activeFile.raw = !RepoStore.activeFile.raw; - RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source'; - }, - - setActiveFiles(file) { - if (RepoStore.isActiveFile(file)) return; - RepoStore.openedFiles = RepoStore.openedFiles - .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i)); - - RepoStore.setActiveToRaw(); - - if (file.binary) { - RepoStore.blobRaw = file.base64; - } else if (file.newContent || file.plain) { - RepoStore.blobRaw = file.newContent || file.plain; - } else { - Service.getRaw(file) - .then((rawResponse) => { - RepoStore.blobRaw = rawResponse.data; - Helper.findOpenedFileFromActive().plain = rawResponse.data; - }).catch(Helper.loadingError); - } - - if (!file.loading && !file.tempFile) { - Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); - } - RepoStore.binary = file.binary; - RepoStore.setActiveLine(-1); - }, - - setFileActivity(file, openedFile, i) { - const activeFile = openedFile; - activeFile.active = file.id === activeFile.id; - - if (activeFile.active) RepoStore.setActiveFile(activeFile, i); - - return activeFile; - }, - - setActiveFile(activeFile, i) { - RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile); - RepoStore.activeFileIndex = i; - }, - - setActiveLine(activeLine) { - if (!isNaN(activeLine)) RepoStore.activeLine = activeLine; - }, - - setActiveToRaw() { - RepoStore.activeFile.raw = false; - // can't get vue to listen to raw for some reason so RepoStore for now. - RepoStore.activeFileLabel = 'Display source'; - }, - - removeFromOpenedFiles(file) { - if (file.type === 'tree') return; - let foundIndex; - RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { - if (openedFile.path === file.path) foundIndex = i; - return openedFile.path !== file.path; - }); - - // remove the file from the sidebar if it is a tempFile - if (file.tempFile) { - RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path)); - } - - // now activate the right tab based on what you closed. - if (RepoStore.openedFiles.length === 0) { - RepoStore.activeFile = {}; - return; - } - - if (RepoStore.openedFiles.length === 1 || foundIndex === 0) { - RepoStore.setActiveFiles(RepoStore.openedFiles[0]); - return; - } - - if (foundIndex && foundIndex > 0) { - RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); - } - }, - - addToOpenedFiles(file) { - const openFile = file; - - const openedFilesAlreadyExists = RepoStore.openedFiles - .some(openedFile => openedFile.path === openFile.path); - - if (openedFilesAlreadyExists) return; - - openFile.changed = false; - openFile.active = true; - RepoStore.openedFiles.push(openFile); - }, - - setActiveFileContents(contents) { - if (!RepoStore.editMode) return; - const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex]; - RepoStore.activeFile.newContent = contents; - RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent; - currentFile.changed = RepoStore.activeFile.changed; - currentFile.newContent = contents; - }, - - toggleBlobView() { - RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview'; - }, - - setViewToPreview() { - RepoStore.currentBlobView = 'repo-preview'; - }, - - // getters - - isActiveFile(file) { - return file && file.id === RepoStore.activeFile.id; - }, - - isPreviewView() { - return RepoStore.currentBlobView === 'repo-preview'; - }, -}; - -export default RepoStore; diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js new file mode 100644 index 00000000000..aab74754f02 --- /dev/null +++ b/app/assets/javascripts/repo/stores/state.js @@ -0,0 +1,23 @@ +export default () => ({ + canCommit: false, + currentBranch: '', + currentBlobView: 'repo-preview', + currentRef: '', + discardPopupOpen: false, + editMode: false, + endpoints: {}, + isRoot: false, + isInitialRoot: false, + loading: false, + onTopOfBranch: false, + openFiles: [], + path: '', + project: { + id: 0, + name: '', + url: '', + }, + parentTreeUrl: '', + previousUrl: '', + tree: [], +}); diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js new file mode 100644 index 00000000000..797c2b1e5b9 --- /dev/null +++ b/app/assets/javascripts/repo/stores/utils.js @@ -0,0 +1,108 @@ +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: '', + renderError: false, + base64: false, +}); + +export const decorateData = (entity, projectUrl = '') => { + const { + id, + type, + url, + name, + icon, + last_commit, + tree_url, + path, + renderError, + content = '', + tempFile = false, + active = false, + opened = false, + changed = false, + parentTreeUrl = '', + level = 0, + base64 = false, + } = entity; + + return { + ...dataStructure(), + id, + type, + name, + url, + tree_url, + path, + level, + tempFile, + icon: `fa-${icon}`, + opened, + active, + parentTreeUrl, + changed, + renderError, + content, + base64, + // eslint-disable-next-line camelcase + lastCommit: last_commit ? { + url: `${projectUrl}/commit/${last_commit.id}`, + message: last_commit.message, + updatedAt: last_commit.committed_date, + } : {}, + }; +}; + +export const findEntry = (state, type, name) => state.tree.find( + f => f.type === type && f.name === name, +); +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); +}; + +export const createTemp = ({ name, path, type, level, changed, content, base64 }) => { + const treePath = path ? `${path}/${name}` : name; + + return decorateData({ + id: new Date().getTime().toString(), + name, + type, + tempFile: true, + path: treePath, + icon: type === 'tree' ? 'folder' : 'file-text-o', + changed, + content, + parentTreeUrl: '', + level, + base64, + renderError: base64, + }); +}; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6a363b1710e..1bb4e3cc345 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,12 +1,3 @@ -.monaco-loader { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: $black-transparent; -} - .modal.popup-dialog { display: block; background-color: $black-transparent; @@ -54,6 +45,7 @@ } .tree-content-holder { + display: -webkit-flex; display: flex; min-height: 300px; } @@ -63,7 +55,9 @@ } .panel-right { + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; width: 80%; height: 100%; @@ -81,10 +75,6 @@ text-decoration: underline; } } - - .cursor { - display: none !important; - } } .blob-no-preview { @@ -94,21 +84,12 @@ } } - &.edit-mode { - .blob-viewer-container { - overflow: hidden; - } - - .monaco-editor.vs { - .cursor { - background: $black; - border-color: $black; - display: block !important; - } - } + &.blob-editor-container { + overflow: hidden; } .blob-viewer-container { + -webkit-flex: 1; flex: 1; overflow: auto; @@ -138,6 +119,7 @@ } #tabs { + position: relative; flex-shrink: 0; display: flex; width: 100%; @@ -166,6 +148,10 @@ vertical-align: middle; text-decoration: none; margin-right: 12px; + + &:focus { + outline: none; + } } .close-btn { @@ -312,23 +298,3 @@ width: 100%; } } - -@keyframes swipeRightAppear { - 0% { - transform: scaleX(0.00); - } - - 100% { - transform: scaleX(1.00); - } -} - -@keyframes swipeRightDissapear { - 0% { - transform: scaleX(1.00); - } - - 100% { - transform: scaleX(0.00); - } -} diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 7ea19e6c828..c02f7ee37ed 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -2,14 +2,14 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true - - if show_new_repo? + - if show_new_repo? && can_push_branch?(@project, @ref) .js-new-dropdown - else = render 'projects/tree/old_tree_header' .tree-controls - if show_new_repo? - = render 'shared/repo/editable_mode' + .editable-mode - else = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' diff --git a/app/views/shared/repo/_editable_mode.html.haml b/app/views/shared/repo/_editable_mode.html.haml deleted file mode 100644 index 73fdb8b523f..00000000000 --- a/app/views/shared/repo/_editable_mode.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.editable-mode - %repo-edit-button diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 7861f92b33f..5867ea58378 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,11 +1,12 @@ #repo{ data: { root: @path.empty?.to_s, + root_url: project_tree_path(project), url: content_url, + current_branch: @ref, + 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 } } diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index a67ec891e7c..ed3b52a5790 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -28,8 +28,6 @@ feature 'Multi-file editor new file', :js do click_button('Create file') end - find('.inputarea').send_keys('file content') - fill_in('commit-message', with: 'commit message') click_button('Commit 1 file') diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index b71136c4114..34acdfbfba9 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -1,3 +1,8 @@ +export const createComponentWithStore = (Component, store, propsData = {}) => new Component({ + store, + propsData, +}); + export default (Component, props = {}, el = null) => new Component({ propsData: props, }).$mount(el); diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js index c9c5ce096fc..9a705a1f0ed 100644 --- a/spec/javascripts/repo/components/new_branch_form_spec.js +++ b/spec/javascripts/repo/components/new_branch_form_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import newBranchForm from '~/repo/components/new_branch_form.vue'; -import eventHub from '~/repo/event_hub'; -import RepoStore from '~/repo/stores/repo_store'; -import createComponent from '../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { resetStore } from '../helpers'; describe('Multi-file editor new branch form', () => { let vm; @@ -10,17 +10,17 @@ describe('Multi-file editor new branch form', () => { beforeEach(() => { const Component = Vue.extend(newBranchForm); - RepoStore.currentBranch = 'master'; + vm = createComponentWithStore(Component, store); - vm = createComponent(Component, { - currentBranch: RepoStore.currentBranch, - }); + vm.$store.state.currentBranch = 'master'; + + vm.$mount(); }); afterEach(() => { vm.$destroy(); - RepoStore.currentBranch = ''; + resetStore(vm.$store); }); describe('template', () => { @@ -48,6 +48,10 @@ describe('Multi-file editor new branch form', () => { }); describe('submitNewBranch', () => { + beforeEach(() => { + spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve()); + }); + it('sets to loading', () => { vm.submitNewBranch(); @@ -66,57 +70,45 @@ describe('Multi-file editor new branch form', () => { }); }); - it('emits an event with branchName', () => { - spyOn(eventHub, '$emit'); - + it('calls createdNewBranch with branchName', () => { vm.branchName = 'testing'; vm.submitNewBranch(); - expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranch', 'testing'); + expect(vm.createNewBranch).toHaveBeenCalledWith('testing'); }); }); - describe('showErrorMessage', () => { - it('sets loading to false', () => { - vm.loading = true; - - vm.showErrorMessage(); - - expect(vm.loading).toBeFalsy(); - }); - - it('creates flash element', () => { - vm.showErrorMessage('error message'); - - expect(vm.$el.querySelector('.flash-alert')).not.toBeNull(); - expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message'); + describe('submitNewBranch with error', () => { + beforeEach(() => { + spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({ + json: () => Promise.resolve({ + message: 'error message', + }), + })); }); - }); - describe('createdNewBranch', () => { - it('set loading to false', () => { + it('sets loading to false', (done) => { vm.loading = true; - vm.createdNewBranch(); - - expect(vm.loading).toBeFalsy(); - }); - - it('resets branch name', () => { - vm.branchName = 'testing'; + vm.submitNewBranch(); - vm.createdNewBranch(); + setTimeout(() => { + expect(vm.loading).toBeFalsy(); - expect(vm.branchName).toBe(''); + done(); + }); }); - it('sets the dropdown toggle text', () => { - vm.dropdownText = document.createElement('span'); + it('creates flash element', (done) => { + vm.submitNewBranch(); - vm.createdNewBranch('branch name'); + setTimeout(() => { + expect(vm.$el.querySelector('.flash-alert')).not.toBeNull(); + expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message'); - expect(vm.dropdownText.textContent).toBe('branch name'); + done(); + }); }); }); }); diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index ddffef53300..93b10fc1fee 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -1,9 +1,8 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import newDropdown from '~/repo/components/new_dropdown/index.vue'; -import RepoStore from '~/repo/stores/repo_store'; -import RepoHelper from '~/repo/helpers/repo_helper'; -import eventHub from '~/repo/event_hub'; -import createComponent from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; describe('new dropdown component', () => { let vm; @@ -11,15 +10,17 @@ describe('new dropdown component', () => { beforeEach(() => { const component = Vue.extend(newDropdown); - vm = createComponent(component); + vm = createComponentWithStore(component, store); + + vm.$store.state.path = ''; + + vm.$mount(); }); afterEach(() => { vm.$destroy(); - RepoStore.files = []; - RepoStore.openedFiles = []; - RepoStore.setViewToPreview(); + resetStore(vm.$store); }); it('renders new file and new directory links', () => { @@ -67,159 +68,4 @@ describe('new dropdown component', () => { .catch(done.fail); }); }); - - describe('createEntryInStore', () => { - ['tree', 'blob'].forEach((type) => { - describe(type, () => { - it('closes modal after creating file', () => { - vm.openModal = true; - - eventHub.$emit('createNewEntry', { - name: 'testing', - type, - toggleModal: true, - }); - - expect(vm.openModal).toBeFalsy(); - }); - - it('sets editMode to true', () => { - eventHub.$emit('createNewEntry', { - name: 'testing', - type, - }); - - expect(RepoStore.editMode).toBeTruthy(); - }); - - it('toggles blob view', () => { - eventHub.$emit('createNewEntry', { - name: 'testing', - type, - }); - - expect(RepoStore.isPreviewView()).toBeFalsy(); - }); - - it('adds file into activeFiles', () => { - eventHub.$emit('createNewEntry', { - name: 'testing', - type, - }); - - expect(RepoStore.openedFiles.length).toBe(1); - }); - - it(`creates ${type} in the current stores path`, () => { - RepoStore.path = 'testing'; - - eventHub.$emit('createNewEntry', { - name: 'testing/app', - type, - }); - - expect(RepoStore.files[0].path).toBe('testing/app'); - expect(RepoStore.files[0].name).toBe('app'); - - if (type === 'tree') { - expect(RepoStore.files[0].files.length).toBe(1); - } - - RepoStore.path = ''; - }); - }); - }); - - describe('file', () => { - it('creates new file', () => { - eventHub.$emit('createNewEntry', { - name: 'testing', - type: 'blob', - }); - - expect(RepoStore.files.length).toBe(1); - expect(RepoStore.files[0].name).toBe('testing'); - expect(RepoStore.files[0].type).toBe('blob'); - expect(RepoStore.files[0].tempFile).toBeTruthy(); - }); - - it('does not create temp file when file already exists', () => { - RepoStore.files.push(RepoHelper.serializeRepoEntity('blob', { - name: 'testing', - })); - - eventHub.$emit('createNewEntry', { - name: 'testing', - type: 'blob', - }); - - expect(RepoStore.files.length).toBe(1); - expect(RepoStore.files[0].name).toBe('testing'); - expect(RepoStore.files[0].type).toBe('blob'); - expect(RepoStore.files[0].tempFile).toBeUndefined(); - }); - }); - - describe('tree', () => { - it('creates new tree', () => { - eventHub.$emit('createNewEntry', { - name: 'testing', - type: 'tree', - }); - - expect(RepoStore.files.length).toBe(1); - expect(RepoStore.files[0].name).toBe('testing'); - expect(RepoStore.files[0].type).toBe('tree'); - expect(RepoStore.files[0].tempFile).toBeTruthy(); - expect(RepoStore.files[0].files.length).toBe(1); - expect(RepoStore.files[0].files[0].name).toBe('.gitkeep'); - }); - - it('creates multiple trees when entryName has slashes', () => { - eventHub.$emit('createNewEntry', { - name: 'app/test', - type: 'tree', - }); - - expect(RepoStore.files.length).toBe(1); - expect(RepoStore.files[0].name).toBe('app'); - expect(RepoStore.files[0].files[0].name).toBe('test'); - expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep'); - }); - - it('creates tree in existing tree', () => { - RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', { - name: 'app', - })); - - eventHub.$emit('createNewEntry', { - name: 'app/test', - type: 'tree', - }); - - expect(RepoStore.files.length).toBe(1); - expect(RepoStore.files[0].name).toBe('app'); - expect(RepoStore.files[0].tempFile).toBeUndefined(); - expect(RepoStore.files[0].files[0].tempFile).toBeTruthy(); - expect(RepoStore.files[0].files[0].name).toBe('test'); - expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep'); - }); - - it('does not create new tree when already exists', () => { - RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', { - name: 'app', - })); - - eventHub.$emit('createNewEntry', { - name: 'app', - type: 'tree', - }); - - expect(RepoStore.files.length).toBe(1); - expect(RepoStore.files[0].name).toBe('app'); - expect(RepoStore.files[0].tempFile).toBeUndefined(); - expect(RepoStore.files[0].files.length).toBe(0); - }); - }); - }); }); diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js index d9fd9b9a595..1ff7590ec79 100644 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; -import RepoStore from '~/repo/stores/repo_store'; +import store from '~/repo/stores'; import modal from '~/repo/components/new_dropdown/modal.vue'; -import eventHub from '~/repo/event_hub'; -import createComponent from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; describe('new file modal component', () => { const Component = Vue.extend(modal); @@ -11,18 +11,18 @@ describe('new file modal component', () => { afterEach(() => { vm.$destroy(); - RepoStore.files = []; - RepoStore.openedFiles = []; - RepoStore.setViewToPreview(); + resetStore(vm.$store); }); ['tree', 'blob'].forEach((type) => { describe(type, () => { beforeEach(() => { - vm = createComponent(Component, { + vm = createComponentWithStore(Component, store, { type, - currentPath: RepoStore.path, - }); + path: '', + }).$mount(); + + vm.entryName = 'testing'; }); it(`sets modal title as ${type}`, () => { @@ -42,39 +42,157 @@ describe('new file modal component', () => { expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`); }); + + describe('createEntryInStore', () => { + it('calls createTempEntry', () => { + spyOn(vm, 'createTempEntry'); + + vm.createEntryInStore(); + + expect(vm.createTempEntry).toHaveBeenCalledWith({ + name: 'testing', + type, + }); + }); + + it('sets editMode to true', (done) => { + vm.createEntryInStore(); + + setTimeout(() => { + expect(vm.$store.state.editMode).toBeTruthy(); + + done(); + }); + }); + + it('toggles blob view', (done) => { + vm.createEntryInStore(); + + setTimeout(() => { + expect(vm.$store.state.currentBlobView).toBe('repo-editor'); + + done(); + }); + }); + + it('opens newly created file', (done) => { + vm.createEntryInStore(); + + setTimeout(() => { + expect(vm.$store.state.openFiles.length).toBe(1); + expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep'); + + done(); + }); + }); + + it(`creates ${type} in the current stores path`, (done) => { + vm.$store.state.path = 'app'; + + vm.createEntryInStore(); + + setTimeout(() => { + expect(vm.$store.state.tree[0].path).toBe('app/testing'); + expect(vm.$store.state.tree[0].name).toBe('testing'); + + if (type === 'tree') { + expect(vm.$store.state.tree[0].tree.length).toBe(1); + } + + done(); + }); + }); + + if (type === 'blob') { + it('creates new file', (done) => { + vm.createEntryInStore(); + + setTimeout(() => { + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe('testing'); + expect(vm.$store.state.tree[0].type).toBe('blob'); + expect(vm.$store.state.tree[0].tempFile).toBeTruthy(); + + done(); + }); + }); + + it('does not create temp file when file already exists', (done) => { + vm.$store.state.tree.push(file('testing', '1', type)); + + vm.createEntryInStore(); + + setTimeout(() => { + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe('testing'); + expect(vm.$store.state.tree[0].type).toBe('blob'); + expect(vm.$store.state.tree[0].tempFile).toBeFalsy(); + + done(); + }); + }); + } else { + it('creates new tree', () => { + vm.createEntryInStore(); + + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe('testing'); + expect(vm.$store.state.tree[0].type).toBe('tree'); + expect(vm.$store.state.tree[0].tempFile).toBeTruthy(); + expect(vm.$store.state.tree[0].tree.length).toBe(1); + expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep'); + }); + + it('creates multiple trees when entryName has slashes', () => { + vm.entryName = 'app/test'; + vm.createEntryInStore(); + + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe('app'); + expect(vm.$store.state.tree[0].tree[0].name).toBe('test'); + expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep'); + }); + + it('creates tree in existing tree', () => { + vm.$store.state.tree.push(file('app', '1', 'tree')); + + vm.entryName = 'app/test'; + vm.createEntryInStore(); + + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe('app'); + expect(vm.$store.state.tree[0].tempFile).toBeFalsy(); + expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy(); + expect(vm.$store.state.tree[0].tree[0].name).toBe('test'); + expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep'); + }); + + it('does not create new tree when already exists', () => { + vm.$store.state.tree.push(file('app', '1', 'tree')); + + vm.entryName = 'app'; + vm.createEntryInStore(); + + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe('app'); + expect(vm.$store.state.tree[0].tempFile).toBeFalsy(); + expect(vm.$store.state.tree[0].tree.length).toBe(0); + }); + } + }); }); }); it('focuses field on mount', () => { document.body.innerHTML += '<div class="js-test"></div>'; - vm = createComponent(Component, { + vm = createComponentWithStore(Component, store, { type: 'tree', - currentPath: RepoStore.path, - }, '.js-test'); + path: '', + }).$mount('.js-test'); expect(document.activeElement).toBe(vm.$refs.fieldName); vm.$el.remove(); }); - - describe('createEntryInStore', () => { - it('emits createNewEntry event', () => { - spyOn(eventHub, '$emit'); - - vm = createComponent(Component, { - type: 'tree', - currentPath: RepoStore.path, - }); - vm.entryName = 'testing'; - - vm.createEntryInStore(); - - expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { - name: 'testing', - type: 'tree', - toggleModal: true, - }); - }); - }); }); diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js index 31878e9d327..bf7893029b1 100644 --- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js @@ -1,7 +1,8 @@ import Vue from 'vue'; import upload from '~/repo/components/new_dropdown/upload.vue'; -import eventHub from '~/repo/event_hub'; -import createComponent from '../../../helpers/vue_mount_component_helper'; +import store from '~/repo/stores'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; describe('new dropdown upload', () => { let vm; @@ -9,13 +10,17 @@ describe('new dropdown upload', () => { beforeEach(() => { const Component = Vue.extend(upload); - vm = createComponent(Component, { - currentPath: '', + vm = createComponentWithStore(Component, store, { + path: '', }); + + vm.$mount(); }); afterEach(() => { vm.$destroy(); + + resetStore(vm.$store); }); describe('readFile', () => { @@ -56,45 +61,43 @@ describe('new dropdown upload', () => { name: 'file', }; - beforeEach(() => { - spyOn(eventHub, '$emit'); - }); - - it('emits createNewEntry event', () => { + it('creates new file', (done) => { vm.createFile(target, file, true); - expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { - name: 'file', - type: 'blob', - content: 'content', - toggleModal: false, - base64: false, - }, true); + vm.$nextTick(() => { + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe(file.name); + expect(vm.$store.state.tree[0].content).toBe(target.result); + + done(); + }); }); - it('createNewEntry event name contains current path', () => { - vm.currentPath = 'testing'; + it('creates new file in path', (done) => { + vm.$store.state.path = 'testing'; vm.createFile(target, file, true); - expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { - name: 'testing/file', - type: 'blob', - content: 'content', - toggleModal: false, - base64: false, - }, true); + vm.$nextTick(() => { + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe(file.name); + expect(vm.$store.state.tree[0].content).toBe(target.result); + expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`); + + done(); + }); }); - it('splits content on base64 if binary', () => { + it('splits content on base64 if binary', (done) => { vm.createFile(binaryTarget, file, false); - expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { - name: 'file', - type: 'blob', - content: 'base64content', - toggleModal: false, - base64: true, - }, false); + vm.$nextTick(() => { + expect(vm.$store.state.tree.length).toBe(1); + expect(vm.$store.state.tree[0].name).toBe(file.name); + expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]); + expect(vm.$store.state.tree[0].base64).toBe(true); + + done(); + }); }); }); }); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index e09d593f04c..0f991e1b727 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -1,56 +1,43 @@ import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; import repoCommitSection from '~/repo/components/repo_commit_section.vue'; -import RepoStore from '~/repo/stores/repo_store'; -import RepoService from '~/repo/services/repo_service'; import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; +import { file, resetStore } from '../helpers'; describe('RepoCommitSection', () => { - const branch = 'master'; - const projectUrl = 'projectUrl'; - let changedFiles; - let openedFiles; + let vm; - RepoStore.projectUrl = projectUrl; - - function createComponent(el) { + function createComponent() { const RepoCommitSection = Vue.extend(repoCommitSection); - return new RepoCommitSection().$mount(el); + const comp = new RepoCommitSection({ + store, + }).$mount(); + + comp.$store.state.currentBranch = 'master'; + comp.$store.state.openFiles = [file(), file()]; + comp.$store.state.openFiles.forEach(f => Object.assign(f, { + changed: true, + content: 'testing', + })); + + return comp.$mount(); } beforeEach(() => { - // Create a copy for each test because these can get modified directly - changedFiles = [{ - id: 0, - changed: true, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, - path: 'dir/file0.ext', - newContent: 'a', - }, { - id: 1, - changed: true, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`, - path: 'dir/file1.ext', - newContent: 'b', - }]; - openedFiles = changedFiles.concat([{ - id: 2, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, - path: 'dir/file2.ext', - changed: false, - }]); + vm = createComponent(); }); - it('renders a commit section', () => { - RepoStore.isCommitable = true; - RepoStore.currentBranch = branch; - RepoStore.targetBranch = branch; - RepoStore.openedFiles = openedFiles; + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); - const vm = createComponent(); + it('renders a commit section', () => { const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')]; - const commitMessage = vm.$el.querySelector('#commit-message'); - const submitCommit = vm.$refs.submitCommit; + const submitCommit = vm.$el.querySelector('.btn'); const targetBranch = vm.$el.querySelector('.target-branch'); expect(vm.$el.querySelector(':scope > form')).toBeTruthy(); @@ -58,160 +45,70 @@ describe('RepoCommitSection', () => { expect(changedFileElements.length).toEqual(2); changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path); + expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path); }); - expect(commitMessage.tagName).toEqual('TEXTAREA'); - expect(commitMessage.name).toEqual('commit-message'); - expect(submitCommit.type).toEqual('submit'); expect(submitCommit.disabled).toBeTruthy(); expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy(); expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files'); expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch'); - expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch); - }); - - it('does not render if not isCommitable', () => { - RepoStore.isCommitable = false; - RepoStore.openedFiles = [{ - id: 0, - changed: true, - }]; - - const vm = createComponent(); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); - - it('does not render if no changedFiles', () => { - RepoStore.isCommitable = true; - RepoStore.openedFiles = []; - - const vm = createComponent(); - - expect(vm.$el.innerHTML).toBeFalsy(); + expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual('master'); }); describe('when submitting', () => { - let el; - let vm; - const projectId = 'projectId'; - const commitMessage = 'commitMessage'; - - beforeEach((done) => { - RepoStore.isCommitable = true; - RepoStore.currentBranch = branch; - RepoStore.targetBranch = branch; - RepoStore.openedFiles = openedFiles; - RepoStore.projectId = projectId; - - // We need to append to body to get form `submit` events working - // Otherwise we run into, "Form submission canceled because the form is not connected" - // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm - el = document.createElement('div'); - document.body.appendChild(el); - - vm = createComponent(el); - vm.commitMessage = commitMessage; - - spyOn(vm, 'tryCommit').and.callThrough(); - spyOn(vm, 'redirectToNewMr').and.stub(); - spyOn(vm, 'redirectToBranch').and.stub(); - spyOn(RepoService, 'commitFiles').and.returnValue(Promise.resolve()); - spyOn(RepoService, 'getBranch').and.returnValue(Promise.resolve({ - commit: { - id: 1, - short_id: 1, - }, - })); - - // Wait for the vm data to be in place - Vue.nextTick(() => { - done(); - }); - }); + let changedFiles; - afterEach(() => { - vm.$destroy(); - el.remove(); - RepoStore.openedFiles = []; - }); + beforeEach(() => { + vm.commitMessage = 'testing'; + changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles)); - it('shows commit message', () => { - const commitMessageEl = vm.$el.querySelector('#commit-message'); - expect(commitMessageEl.value).toBe(commitMessage); + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + short_id: '1', + stats: {}, + })); }); it('allows you to submit', () => { - const submitCommit = vm.$refs.submitCommit; - expect(submitCommit.disabled).toBeFalsy(); + expect(vm.$el.querySelector('.btn').disabled).toBeTruthy(); }); - it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => { - const submitCommit = vm.$refs.submitCommit; - submitCommit.click(); + it('submits commit', (done) => { + vm.makeCommit(); // Wait for the branch check to finish getSetTimeoutPromise() .then(() => Vue.nextTick()) .then(() => { - expect(vm.tryCommit).toHaveBeenCalled(); - expect(submitCommit.querySelector('.js-commit-loading-icon')).toBeTruthy(); - expect(vm.redirectToBranch).toHaveBeenCalled(); - - const args = RepoService.commitFiles.calls.allArgs()[0]; - const { commit_message, actions, branch: payloadBranch } = args[0]; + const args = service.commit.calls.allArgs()[0]; + const { commit_message, actions, branch: payloadBranch } = args[1]; - expect(commit_message).toBe(commitMessage); + expect(commit_message).toBe('testing'); expect(actions.length).toEqual(2); - expect(payloadBranch).toEqual(branch); + expect(payloadBranch).toEqual('master'); expect(actions[0].action).toEqual('update'); expect(actions[1].action).toEqual('update'); - expect(actions[0].content).toEqual(openedFiles[0].newContent); - expect(actions[1].content).toEqual(openedFiles[1].newContent); - expect(actions[0].file_path).toEqual(openedFiles[0].path); - expect(actions[1].file_path).toEqual(openedFiles[1].path); + expect(actions[0].content).toEqual(changedFiles[0].content); + expect(actions[1].content).toEqual(changedFiles[1].content); + expect(actions[0].file_path).toEqual(changedFiles[0].path); + expect(actions[1].file_path).toEqual(changedFiles[1].path); }) .then(done) .catch(done.fail); }); it('redirects to MR creation page if start new MR checkbox checked', (done) => { + spyOn(gl.utils, 'visitUrl'); vm.startNewMR = true; - Vue.nextTick() - .then(() => { - const submitCommit = vm.$refs.submitCommit; - submitCommit.click(); - }) - // Wait for the branch check to finish - .then(() => getSetTimeoutPromise()) + vm.makeCommit(); + + getSetTimeoutPromise() + .then(() => Vue.nextTick()) .then(() => { - expect(vm.redirectToNewMr).toHaveBeenCalled(); + expect(gl.utils.visitUrl).toHaveBeenCalled(); }) .then(done) .catch(done.fail); }); }); - - describe('methods', () => { - describe('resetCommitState', () => { - it('should reset store vars and scroll to top', () => { - const vm = { - submitCommitsLoading: true, - changedFiles: new Array(10), - openedFiles: new Array(3), - commitMessage: 'commitMessage', - editMode: true, - }; - - repoCommitSection.methods.resetCommitState.call(vm); - - expect(vm.submitCommitsLoading).toEqual(false); - expect(vm.changedFiles).toEqual([]); - expect(vm.commitMessage).toEqual(''); - expect(vm.editMode).toEqual(false); - }); - }); - }); }); diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js index dff2fac191d..44018464b35 100644 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ b/spec/javascripts/repo/components/repo_edit_button_spec.js @@ -1,45 +1,83 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repoEditButton from '~/repo/components/repo_edit_button.vue'; -import RepoStore from '~/repo/stores/repo_store'; +import { file, resetStore } from '../helpers'; describe('RepoEditButton', () => { - function createComponent() { + let vm; + + beforeEach(() => { + const f = file(); const RepoEditButton = Vue.extend(repoEditButton); - return new RepoEditButton().$mount(); - } + vm = new RepoEditButton({ + store, + }); + + f.active = true; + vm.$store.dispatch('setInitialData', { + canCommit: true, + onTopOfBranch: true, + }); + vm.$store.state.openFiles.push(f); + }); afterEach(() => { - RepoStore.openedFiles = []; + vm.$destroy(); + + resetStore(vm.$store); }); - it('renders an edit button that toggles the view state', (done) => { - RepoStore.isCommitable = true; - RepoStore.changedFiles = []; - RepoStore.binary = false; - RepoStore.openedFiles = [{}, {}]; + it('renders an edit button', () => { + vm.$mount(); + + expect(vm.$el.querySelector('.btn')).not.toBeNull(); + expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit'); + }); - const vm = createComponent(); + it('renders edit button with cancel text', () => { + vm.$store.state.editMode = true; - expect(vm.$el.tagName).toEqual('BUTTON'); - expect(vm.$el.textContent).toMatch('Edit'); + vm.$mount(); - spyOn(vm, 'editCancelClicked').and.callThrough(); + expect(vm.$el.querySelector('.btn')).not.toBeNull(); + expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); + }); - vm.$el.click(); + it('toggles edit mode on click', (done) => { + vm.$mount(); + + vm.$el.querySelector('.btn').click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); - Vue.nextTick(() => { - expect(vm.editCancelClicked).toHaveBeenCalled(); - expect(vm.$el.textContent).toMatch('Cancel edit'); done(); }); }); - it('does not render if not isCommitable', () => { - RepoStore.isCommitable = false; + describe('discardPopupOpen', () => { + beforeEach(() => { + vm.$store.state.discardPopupOpen = true; + vm.$store.state.editMode = true; + vm.$store.state.openFiles[0].changed = true; + + vm.$mount(); + }); + + it('renders popup', () => { + expect(vm.$el.querySelector('.modal')).not.toBeNull(); + }); + + it('removes all changed files', (done) => { + vm.$el.querySelector('.btn-warning').click(); - const vm = createComponent(); + vm.$nextTick(() => { + expect(vm.$store.getters.changedFiles.length).toBe(0); + expect(vm.$el.querySelector('.modal')).toBeNull(); - expect(vm.$el.innerHTML).toBeUndefined(); + done(); + }); + }); }); }); diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index a25a600b3be..82f914b7c9d 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -1,52 +1,46 @@ import Vue from 'vue'; -import RepoStore from '~/repo/stores/repo_store'; +import store from '~/repo/stores'; import repoEditor from '~/repo/components/repo_editor.vue'; +import { file, resetStore } from '../helpers'; describe('RepoEditor', () => { + let vm; + beforeEach(() => { + const f = file(); const RepoEditor = Vue.extend(repoEditor); - this.vm = new RepoEditor().$mount(); + vm = new RepoEditor({ + store, + }); + + f.active = true; + f.tempFile = true; + vm.$store.state.openFiles.push(f); + vm.monaco = true; + + vm.$mount(); }); afterEach(() => { - RepoStore.openedFiles = []; + vm.$destroy(); + + resetStore(vm.$store); }); it('renders an ide container', (done) => { - this.vm.openedFiles = ['idiidid']; - this.vm.binary = false; - Vue.nextTick(() => { - expect(this.vm.shouldHideEditor).toBe(false); - expect(this.vm.$el.id).toEqual('ide'); - expect(this.vm.$el.tagName).toBe('DIV'); + expect(vm.shouldHideEditor).toBeFalsy(); done(); }); }); - describe('when there are no open files', () => { - it('does not render the ide', (done) => { - this.vm.openedFiles = []; - - Vue.nextTick(() => { - expect(this.vm.shouldHideEditor).toBe(true); - expect(this.vm.$el.tagName).not.toBeDefined(); - done(); - }); - }); - }); - describe('when open file is binary and not raw', () => { it('does not render the IDE', (done) => { - this.vm.binary = true; - this.vm.activeFile = { - raw: false, - }; + vm.$store.getters.activeFile.binary = true; Vue.nextTick(() => { - expect(this.vm.shouldHideEditor).toBe(true); - expect(this.vm.$el.tagName).not.toBeDefined(); + expect(vm.shouldHideEditor).toBeTruthy(); done(); }); }); diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js index 111c83ee50d..d6e255e4810 100644 --- a/spec/javascripts/repo/components/repo_file_buttons_spec.js +++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js @@ -1,72 +1,49 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repoFileButtons from '~/repo/components/repo_file_buttons.vue'; -import RepoStore from '~/repo/stores/repo_store'; +import { file, resetStore } from '../helpers'; describe('RepoFileButtons', () => { - const activeFile = { - extension: 'md', - url: 'url', - raw_path: 'raw_path', - blame_path: 'blame_path', - commits_path: 'commits_path', - permalink: 'permalink', - }; + const activeFile = file(); + let vm; function createComponent() { const RepoFileButtons = Vue.extend(repoFileButtons); - return new RepoFileButtons().$mount(); + activeFile.rawPath = 'test'; + activeFile.blamePath = 'test'; + activeFile.commitsPath = 'test'; + activeFile.active = true; + store.state.openFiles.push(activeFile); + + return new RepoFileButtons({ + store, + }).$mount(); } afterEach(() => { - RepoStore.openedFiles = []; - }); - - it('renders Raw, Blame, History, Permalink and Preview toggle', () => { - const activeFileLabel = 'activeFileLabel'; - RepoStore.openedFiles = new Array(1); - RepoStore.activeFile = activeFile; - RepoStore.activeFileLabel = activeFileLabel; - RepoStore.editMode = true; - RepoStore.binary = false; + vm.$destroy(); - const vm = createComponent(); - const raw = vm.$el.querySelector('.raw'); - const blame = vm.$el.querySelector('.blame'); - const history = vm.$el.querySelector('.history'); - - expect(raw.href).toMatch(`/${activeFile.raw_path}`); - expect(raw.textContent.trim()).toEqual('Raw'); - expect(blame.href).toMatch(`/${activeFile.blame_path}`); - expect(blame.textContent.trim()).toEqual('Blame'); - expect(history.href).toMatch(`/${activeFile.commits_path}`); - expect(history.textContent.trim()).toEqual('History'); - expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); - expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel); + resetStore(vm.$store); }); - it('triggers rawPreviewToggle on preview click', () => { - RepoStore.openedFiles = new Array(1); - RepoStore.activeFile = activeFile; - RepoStore.editMode = true; - - const vm = createComponent(); - const preview = vm.$el.querySelector('.preview'); - - spyOn(vm, 'rawPreviewToggle'); - - preview.click(); - - expect(vm.rawPreviewToggle).toHaveBeenCalled(); - }); + it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => { + vm = createComponent(); - it('does not render preview toggle if not canPreview', () => { - activeFile.extension = 'js'; - RepoStore.openedFiles = new Array(1); - RepoStore.activeFile = activeFile; + vm.$nextTick(() => { + const raw = vm.$el.querySelector('.raw'); + const blame = vm.$el.querySelector('.blame'); + const history = vm.$el.querySelector('.history'); - const vm = createComponent(); + expect(raw.href).toMatch(`/${activeFile.rawPath}`); + expect(raw.textContent.trim()).toEqual('Raw'); + expect(blame.href).toMatch(`/${activeFile.blamePath}`); + expect(blame.textContent.trim()).toEqual('Blame'); + expect(history.href).toMatch(`/${activeFile.commitsPath}`); + expect(history.textContent.trim()).toEqual('History'); + expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); - expect(vm.$el.querySelector('.preview')).toBeFalsy(); + done(); + }); }); }); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 8403df9be64..c45f8a18d1f 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -1,32 +1,29 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repoFile from '~/repo/components/repo_file.vue'; -import RepoStore from '~/repo/stores/repo_store'; -import eventHub from '~/repo/event_hub'; -import { file } from '../mock_data'; +import { file, resetStore } from '../helpers'; describe('RepoFile', () => { const updated = 'updated'; - const otherFile = { - id: 'test', - html: '<p class="file-content">html</p>', - pageTitle: 'otherpageTitle', - }; + let vm; function createComponent(propsData) { const RepoFile = Vue.extend(repoFile); return new RepoFile({ + store, propsData, }).$mount(); } - beforeEach(() => { - RepoStore.openedFiles = []; + afterEach(() => { + resetStore(vm.$store); }); it('renders link, icon, name and last commit details', () => { const RepoFile = Vue.extend(repoFile); - const vm = new RepoFile({ + vm = new RepoFile({ + store, propsData: { file: file(), }, @@ -47,23 +44,17 @@ describe('RepoFile', () => { }); it('does render if hasFiles is true and is loading tree', () => { - const vm = createComponent({ + vm = createComponent({ file: file(), }); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); }); - it('sets the document title correctly', () => { - RepoStore.setActiveFiles(otherFile); - - expect(document.title.trim()).toEqual(otherFile.pageTitle); - }); - it('renders a spinner if the file is loading', () => { const f = file(); f.loading = true; - const vm = createComponent({ + vm = createComponent({ file: f, }); @@ -71,32 +62,34 @@ describe('RepoFile', () => { expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`); }); - it('does not render commit message and datetime if mini', () => { - RepoStore.openedFiles.push(file()); - - const vm = createComponent({ + it('does not render commit message and datetime if mini', (done) => { + vm = createComponent({ file: file(), }); + vm.$store.state.openFiles.push(vm.file); - expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); - expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); + vm.$nextTick(() => { + expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); + expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); + + done(); + }); }); - it('fires linkClicked when the link is clicked', () => { - const vm = createComponent({ + it('fires clickedTreeRow when the link is clicked', () => { + vm = createComponent({ file: file(), }); - spyOn(vm, 'linkClicked'); + spyOn(vm, 'clickedTreeRow'); vm.$el.click(); - expect(vm.linkClicked).toHaveBeenCalledWith(vm.file); + expect(vm.clickedTreeRow).toHaveBeenCalledWith(vm.file); }); describe('submodule', () => { let f; - let vm; beforeEach(() => { f = file('submodule name', '123456789'); @@ -119,20 +112,4 @@ describe('RepoFile', () => { expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678'); }); }); - - describe('methods', () => { - describe('linkClicked', () => { - it('$emits fileNameClicked with file obj', () => { - spyOn(eventHub, '$emit'); - - const vm = createComponent({ - file: file(), - }); - - vm.linkClicked(vm.file); - - expect(eventHub.$emit).toHaveBeenCalledWith('fileNameClicked', vm.file); - }); - }); - }); }); diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js index e9f95a02028..031f2a9c0b2 100644 --- a/spec/javascripts/repo/components/repo_loading_file_spec.js +++ b/spec/javascripts/repo/components/repo_loading_file_spec.js @@ -1,13 +1,16 @@ import Vue from 'vue'; -import RepoStore from '~/repo/stores/repo_store'; +import store from '~/repo/stores'; import repoLoadingFile from '~/repo/components/repo_loading_file.vue'; +import { resetStore } from '../helpers'; describe('RepoLoadingFile', () => { - function createComponent(propsData) { + let vm; + + function createComponent() { const RepoLoadingFile = Vue.extend(repoLoadingFile); return new RepoLoadingFile({ - propsData, + store, }).$mount(); } @@ -30,33 +33,30 @@ describe('RepoLoadingFile', () => { } afterEach(() => { - RepoStore.openedFiles = []; + vm.$destroy(); + + resetStore(vm.$store); }); it('renders 3 columns of animated LoC', () => { - const vm = createComponent({ - loading: { - tree: true, - }, - hasFiles: false, - }); + vm = createComponent(); const columns = [...vm.$el.querySelectorAll('td')]; expect(columns.length).toEqual(3); assertColumns(columns); }); - it('renders 1 column of animated LoC if isMini', () => { - RepoStore.openedFiles = new Array(1); - const vm = createComponent({ - loading: { - tree: true, - }, - hasFiles: false, - }); - const columns = [...vm.$el.querySelectorAll('td')]; + it('renders 1 column of animated LoC if isMini', (done) => { + vm = createComponent(); + vm.$store.state.openFiles.push('test'); - expect(columns.length).toEqual(1); - assertColumns(columns); + vm.$nextTick(() => { + const columns = [...vm.$el.querySelectorAll('td')]; + + expect(columns.length).toEqual(1); + assertColumns(columns); + + done(); + }); }); }); diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js index 4c064f21084..7f82ae36a64 100644 --- a/spec/javascripts/repo/components/repo_prev_directory_spec.js +++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js @@ -1,47 +1,45 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue'; -import eventHub from '~/repo/event_hub'; +import { resetStore } from '../helpers'; describe('RepoPrevDirectory', () => { - function createComponent(propsData) { + let vm; + const parentLink = 'parent'; + function createComponent() { const RepoPrevDirectory = Vue.extend(repoPrevDirectory); - return new RepoPrevDirectory({ - propsData, - }).$mount(); - } - - it('renders a prev dir link', () => { - const prevUrl = 'prevUrl'; - const vm = createComponent({ - prevUrl, + const comp = new RepoPrevDirectory({ + store, }); - const link = vm.$el.querySelector('a'); - spyOn(vm, 'linkClicked'); + comp.$store.state.parentTreeUrl = parentLink; - expect(link.href).toMatch(`/${prevUrl}`); - expect(link.textContent).toEqual('...'); + return comp.$mount(); + } + + beforeEach(() => { + vm = createComponent(); + }); - link.click(); + afterEach(() => { + vm.$destroy(); - expect(vm.linkClicked).toHaveBeenCalledWith(prevUrl); + resetStore(vm.$store); }); - describe('methods', () => { - describe('linkClicked', () => { - it('$emits linkclicked with prevUrl', () => { - const prevUrl = 'prevUrl'; - const vm = createComponent({ - prevUrl, - }); + it('renders a prev dir link', () => { + const link = vm.$el.querySelector('a'); - spyOn(eventHub, '$emit'); + expect(link.href).toMatch(`/${parentLink}`); + expect(link.textContent).toEqual('...'); + }); - vm.linkClicked(prevUrl); + it('clicking row triggers getTreeData', () => { + spyOn(vm, 'getTreeData'); - expect(eventHub.$emit).toHaveBeenCalledWith('goToPreviousDirectoryClicked', prevUrl); - }); - }); + vm.$el.querySelector('td').click(); + + expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink }); }); }); diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js index 4920cf02083..8d1a87494cf 100644 --- a/spec/javascripts/repo/components/repo_preview_spec.js +++ b/spec/javascripts/repo/components/repo_preview_spec.js @@ -1,23 +1,37 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repoPreview from '~/repo/components/repo_preview.vue'; -import RepoStore from '~/repo/stores/repo_store'; +import { file, resetStore } from '../helpers'; describe('RepoPreview', () => { + let vm; + function createComponent() { + const f = file(); const RepoPreview = Vue.extend(repoPreview); - return new RepoPreview().$mount(); + const comp = new RepoPreview({ + store, + }); + + f.active = true; + f.html = 'test'; + + comp.$store.state.openFiles.push(f); + + return comp.$mount(); } - it('renders a div with the activeFile html', () => { - const activeFile = { - html: '<p class="file-content">html</p>', - }; - RepoStore.activeFile = activeFile; + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); - const vm = createComponent(); + it('renders a div with the activeFile html', () => { + vm = createComponent(); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.innerHTML).toContain(activeFile.html); + expect(vm.$el.innerHTML).toContain('test'); }); }); diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 148f275e03d..7cb4dace491 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -1,32 +1,31 @@ import Vue from 'vue'; -import Helper from '~/repo/helpers/repo_helper'; -import RepoService from '~/repo/services/repo_service'; -import RepoStore from '~/repo/stores/repo_store'; +import store from '~/repo/stores'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; -import { file } from '../mock_data'; +import { file, resetStore } from '../helpers'; describe('RepoSidebar', () => { let vm; - function createComponent() { + beforeEach(() => { const RepoSidebar = Vue.extend(repoSidebar); - return new RepoSidebar().$mount(); - } + vm = new RepoSidebar({ + store, + }); + + vm.$store.state.isRoot = true; + vm.$store.state.tree.push(file()); + + vm.$mount(); + }); afterEach(() => { vm.$destroy(); - RepoStore.files = []; - RepoStore.openedFiles = []; + resetStore(vm.$store); }); it('renders a sidebar', () => { - RepoStore.files = [file()]; - RepoStore.openedFiles = []; - RepoStore.isRoot = true; - - vm = createComponent(); const thead = vm.$el.querySelector('thead'); const tbody = vm.$el.querySelector('tbody'); @@ -41,139 +40,36 @@ describe('RepoSidebar', () => { expect(tbody.querySelector('.file')).toBeTruthy(); }); - it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', () => { - RepoStore.openedFiles = [{ - id: 0, - }]; - vm = createComponent(); - - expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); - expect(vm.$el.querySelector('thead')).toBeTruthy(); - expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy(); - }); - - it('renders 5 loading files if tree is loading and not hasFiles', () => { - RepoStore.loading.tree = true; - RepoStore.files = []; - vm = createComponent(); + it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', (done) => { + vm.$store.state.openFiles.push(vm.$store.state.tree[0]); - expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); - }); - - it('renders a prev directory if is not root', () => { - RepoStore.files = [file()]; - RepoStore.isRoot = false; - RepoStore.loading.tree = false; - vm = createComponent(); - - expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); - }); + Vue.nextTick(() => { + expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); + expect(vm.$el.querySelector('thead')).toBeTruthy(); + expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy(); - describe('flattendFiles', () => { - it('returns a flattend array of files', () => { - const f = file(); - f.files.push(file('testing 123')); - const files = [f, file()]; - vm = createComponent(); - vm.files = files; - - expect(vm.flattendFiles.length).toBe(3); - expect(vm.flattendFiles[1].name).toBe('testing 123'); + done(); }); }); - describe('methods', () => { - describe('fileClicked', () => { - it('should fetch data for new file', () => { - spyOn(Helper, 'getContent').and.callThrough(); - RepoStore.files = [file()]; - RepoStore.isRoot = true; - vm = createComponent(); - - vm.fileClicked(RepoStore.files[0]); - - expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[0]); - }); - - it('should not fetch data for already opened files', () => { - const f = file(); - spyOn(Helper, 'getFileFromPath').and.returnValue(f); - spyOn(RepoStore, 'setActiveFiles'); - vm = createComponent(); - vm.fileClicked(f); + it('renders 5 loading files if tree is loading', (done) => { + vm.$store.state.tree = []; + vm.$store.state.loading = true; - expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(f); - }); + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); - it('should hide files in directory if already open', () => { - spyOn(Helper, 'setDirectoryToClosed').and.callThrough(); - const f = file(); - f.opened = true; - f.type = 'tree'; - RepoStore.files = [f]; - vm = createComponent(); - - vm.fileClicked(RepoStore.files[0]); - - expect(Helper.setDirectoryToClosed).toHaveBeenCalledWith(RepoStore.files[0]); - }); - - describe('submodule', () => { - it('opens submodule project URL', () => { - spyOn(gl.utils, 'visitUrl'); - - const f = file(); - f.type = 'submodule'; - - vm = createComponent(); - - vm.fileClicked(f); - - expect(gl.utils.visitUrl).toHaveBeenCalledWith('url'); - }); - }); - }); - - describe('goToPreviousDirectoryClicked', () => { - it('should hide files in directory if already open', () => { - const prevUrl = 'foo/bar'; - vm = createComponent(); - - vm.goToPreviousDirectoryClicked(prevUrl); - - expect(RepoService.url).toEqual(prevUrl); - }); + done(); }); + }); - describe('back button', () => { - beforeEach(() => { - const f = file(); - const file2 = Object.assign({}, file()); - file2.url = 'test'; - RepoStore.files = [f, file2]; - RepoStore.openedFiles = []; - RepoStore.isRoot = true; - - vm = createComponent(); - }); - - it('render previous file when using back button', () => { - spyOn(Helper, 'getContent').and.callThrough(); - - vm.fileClicked(RepoStore.files[1]); - expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[1]); - - history.pushState({ - key: Math.random(), - }, '', RepoStore.files[1].url); - const popEvent = document.createEvent('Event'); - popEvent.initEvent('popstate', true, true); - window.dispatchEvent(popEvent); + it('renders a prev directory if is not root', (done) => { + vm.$store.state.isRoot = false; - expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(RepoStore.files[1].url); + Vue.nextTick(() => { + expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); - window.history.pushState({}, null, '/'); - }); + done(); }); }); }); diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/repo_spec.js index 3558a155728..b32d2c13af8 100644 --- a/spec/javascripts/repo/components/repo_spec.js +++ b/spec/javascripts/repo/components/repo_spec.js @@ -1,9 +1,8 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repo from '~/repo/components/repo.vue'; -import RepoStore from '~/repo/stores/repo_store'; -import Service from '~/repo/services/repo_service'; -import eventHub from '~/repo/event_hub'; -import createComponent from '../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../helpers'; describe('repo component', () => { let vm; @@ -11,86 +10,26 @@ describe('repo component', () => { beforeEach(() => { const Component = Vue.extend(repo); - RepoStore.currentBranch = 'master'; - - vm = createComponent(Component); + vm = createComponentWithStore(Component, store).$mount(); }); afterEach(() => { vm.$destroy(); - RepoStore.currentBranch = ''; + resetStore(vm.$store); }); - describe('createNewBranch', () => { - beforeEach(() => { - spyOn(history, 'pushState'); - }); - - describe('success', () => { - beforeEach(() => { - spyOn(Service, 'createBranch').and.returnValue(Promise.resolve({ - data: { - name: 'test', - }, - })); - }); - - it('calls createBranch with branchName', () => { - eventHub.$emit('createNewBranch', 'test'); - - expect(Service.createBranch).toHaveBeenCalledWith({ - branch: 'test', - ref: RepoStore.currentBranch, - }); - }); - - it('pushes new history state', (done) => { - RepoStore.currentBranch = 'master'; - - spyOn(vm, 'getCurrentLocation').and.returnValue('http://test.com/master'); - - eventHub.$emit('createNewBranch', 'test'); - - setTimeout(() => { - expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'http://test.com/test'); - done(); - }); - }); - - it('updates stores currentBranch', (done) => { - eventHub.$emit('createNewBranch', 'test'); - - setTimeout(() => { - expect(RepoStore.currentBranch).toBe('test'); - - done(); - }); - }); - }); - - describe('failure', () => { - beforeEach(() => { - spyOn(Service, 'createBranch').and.returnValue(Promise.reject({ - response: { - data: { - message: 'test', - }, - }, - })); - }); - - it('emits createNewBranchError event', (done) => { - spyOn(eventHub, '$emit').and.callThrough(); + it('does not render panel right when no files open', () => { + expect(vm.$el.querySelector('.panel-right')).toBeNull(); + }); - eventHub.$emit('createNewBranch', 'test'); + it('renders panel right when files are open', (done) => { + vm.$store.state.tree.push(file()); - setTimeout(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranchError', 'test'); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.panel-right')).toBeNull(); - done(); - }); - }); + done(); }); }); }); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index 37e297437f0..df0ca55aafc 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -1,47 +1,64 @@ import Vue from 'vue'; +import store from '~/repo/stores'; import repoTab from '~/repo/components/repo_tab.vue'; -import RepoStore from '~/repo/stores/repo_store'; +import { file, resetStore } from '../helpers'; describe('RepoTab', () => { + let vm; + function createComponent(propsData) { const RepoTab = Vue.extend(repoTab); return new RepoTab({ + store, propsData, }).$mount(); } + afterEach(() => { + resetStore(vm.$store); + }); + it('renders a close link and a name link', () => { - const tab = { - url: 'url', - name: 'name', - }; - const vm = createComponent({ - tab, + vm = createComponent({ + tab: file(), }); + vm.$store.state.openFiles.push(vm.tab); const close = vm.$el.querySelector('.close-btn'); - const name = vm.$el.querySelector(`a[title="${tab.url}"]`); - - spyOn(vm, 'closeTab'); - spyOn(vm, 'tabClicked'); + const name = vm.$el.querySelector(`a[title="${vm.tab.url}"]`); expect(close.querySelector('.fa-times')).toBeTruthy(); - expect(name.textContent.trim()).toEqual(tab.name); + expect(name.textContent.trim()).toEqual(vm.tab.name); + }); - close.click(); - name.click(); + it('calls setFileActive when clicking tab', () => { + vm = createComponent({ + tab: file(), + }); + + spyOn(vm, 'setFileActive'); + + vm.$el.click(); - expect(vm.closeTab).toHaveBeenCalledWith(tab); - expect(vm.tabClicked).toHaveBeenCalledWith(tab); + expect(vm.setFileActive).toHaveBeenCalledWith(vm.tab); + }); + + it('calls closeFile when clicking close button', () => { + vm = createComponent({ + tab: file(), + }); + + spyOn(vm, 'closeFile'); + + vm.$el.querySelector('.close-btn').click(); + + expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab }); }); it('renders an fa-circle icon if tab is changed', () => { - const tab = { - url: 'url', - name: 'name', - changed: true, - }; - const vm = createComponent({ + const tab = file(); + tab.changed = true; + vm = createComponent({ tab, }); @@ -50,38 +67,41 @@ describe('RepoTab', () => { describe('methods', () => { describe('closeTab', () => { - it('returns undefined and does not $emit if file is changed', () => { - const tab = { - url: 'url', - name: 'name', - changed: true, - }; - const vm = createComponent({ + it('does not close tab if is changed', (done) => { + const tab = file(); + tab.changed = true; + tab.opened = true; + vm = createComponent({ tab, }); - - spyOn(RepoStore, 'removeFromOpenedFiles'); + vm.$store.state.openFiles.push(tab); + vm.$store.dispatch('setFileActive', tab); vm.$el.querySelector('.close-btn').click(); - expect(RepoStore.removeFromOpenedFiles).not.toHaveBeenCalled(); + vm.$nextTick(() => { + expect(tab.opened).toBeTruthy(); + + done(); + }); }); - it('$emits tabclosed event with file obj', () => { - const tab = { - url: 'url', - name: 'name', - changed: false, - }; - const vm = createComponent({ + it('closes tab when clicking close btn', (done) => { + const tab = file('lose'); + tab.opened = true; + vm = createComponent({ tab, }); - - spyOn(RepoStore, 'removeFromOpenedFiles'); + vm.$store.state.openFiles.push(tab); + vm.$store.dispatch('setFileActive', tab); vm.$el.querySelector('.close-btn').click(); - expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(tab); + vm.$nextTick(() => { + expect(tab.opened).toBeFalsy(); + + done(); + }); }); }); }); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index 431129bc866..d0246cc72e6 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -1,35 +1,38 @@ import Vue from 'vue'; -import RepoStore from '~/repo/stores/repo_store'; +import store from '~/repo/stores'; import repoTabs from '~/repo/components/repo_tabs.vue'; +import { file, resetStore } from '../helpers'; describe('RepoTabs', () => { - const openedFiles = [{ - id: 0, - active: true, - }, { - id: 1, - }]; + const openedFiles = [file(), file()]; + let vm; function createComponent() { const RepoTabs = Vue.extend(repoTabs); - return new RepoTabs().$mount(); + return new RepoTabs({ + store, + }).$mount(); } afterEach(() => { - RepoStore.openedFiles = []; + resetStore(vm.$store); }); - it('renders a list of tabs', () => { - RepoStore.openedFiles = openedFiles; + it('renders a list of tabs', (done) => { + vm = createComponent(); + openedFiles[0].active = true; + vm.$store.state.openFiles = openedFiles; - const vm = createComponent(); - const tabs = [...vm.$el.querySelectorAll(':scope > li')]; + vm.$nextTick(() => { + const tabs = [...vm.$el.querySelectorAll(':scope > li')]; - expect(vm.$el.id).toEqual('tabs'); - expect(tabs.length).toEqual(3); - expect(tabs[0].classList.contains('active')).toBeTruthy(); - expect(tabs[1].classList.contains('active')).toBeFalsy(); - expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); + expect(tabs.length).toEqual(3); + expect(tabs[0].classList.contains('active')).toBeTruthy(); + expect(tabs[1].classList.contains('active')).toBeFalsy(); + expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); + + done(); + }); }); }); diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js new file mode 100644 index 00000000000..376c291c64b --- /dev/null +++ b/spec/javascripts/repo/helpers.js @@ -0,0 +1,20 @@ +import { decorateData } from '~/repo/stores/utils'; +import state from '~/repo/stores/state'; + +export const resetStore = (store) => { + store.replaceState(state()); +}; + +export const file = (name = 'name', id = name, type = '') => decorateData({ + id, + type, + icon: 'icon', + url: 'url', + name, + path: name, + last_commit: { + id: '123', + message: 'test', + committed_date: new Date().toISOString(), + }, +}); diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js deleted file mode 100644 index 71e275caf09..00000000000 --- a/spec/javascripts/repo/mock_data.js +++ /dev/null @@ -1,14 +0,0 @@ -import RepoHelper from '~/repo/helpers/repo_helper'; - -// eslint-disable-next-line import/prefer-default-export -export const file = (name = 'name', id = name) => RepoHelper.serializeRepoEntity('blob', { - id, - icon: 'icon', - url: 'url', - name, - last_commit: { - id: '123', - message: 'test', - committed_date: new Date().toISOString(), - }, -}); diff --git a/spec/javascripts/repo/services/repo_service_spec.js b/spec/javascripts/repo/services/repo_service_spec.js deleted file mode 100644 index 6f530770525..00000000000 --- a/spec/javascripts/repo/services/repo_service_spec.js +++ /dev/null @@ -1,171 +0,0 @@ -import axios from 'axios'; -import RepoService from '~/repo/services/repo_service'; -import RepoStore from '~/repo/stores/repo_store'; -import Api from '~/api'; - -describe('RepoService', () => { - it('has default json format param', () => { - expect(RepoService.options.params.format).toBe('json'); - }); - - describe('buildParams', () => { - let newParams; - const url = 'url'; - - beforeEach(() => { - newParams = {}; - - spyOn(Object, 'assign').and.returnValue(newParams); - }); - - it('clones params', () => { - const params = RepoService.buildParams(url); - - expect(Object.assign).toHaveBeenCalledWith({}, RepoService.options.params); - - expect(params).toBe(newParams); - }); - - it('sets and returns viewer params to richif urlIsRichBlob is true', () => { - spyOn(RepoService, 'urlIsRichBlob').and.returnValue(true); - - const params = RepoService.buildParams(url); - - expect(params.viewer).toEqual('rich'); - }); - - it('returns params urlIsRichBlob is false', () => { - spyOn(RepoService, 'urlIsRichBlob').and.returnValue(false); - - const params = RepoService.buildParams(url); - - expect(params.viewer).toBeUndefined(); - }); - - it('calls urlIsRichBlob with the objects url prop if no url arg is provided', () => { - spyOn(RepoService, 'urlIsRichBlob'); - RepoService.url = url; - - RepoService.buildParams(); - - expect(RepoService.urlIsRichBlob).toHaveBeenCalledWith(url); - }); - }); - - describe('urlIsRichBlob', () => { - it('returns true for md extension', () => { - const isRichBlob = RepoService.urlIsRichBlob('url.md'); - - expect(isRichBlob).toBeTruthy(); - }); - - it('returns false for js extension', () => { - const isRichBlob = RepoService.urlIsRichBlob('url.js'); - - expect(isRichBlob).toBeFalsy(); - }); - }); - - describe('getContent', () => { - const params = {}; - const url = 'url'; - const requestPromise = Promise.resolve(); - - beforeEach(() => { - spyOn(RepoService, 'buildParams').and.returnValue(params); - spyOn(axios, 'get').and.returnValue(requestPromise); - }); - - it('calls buildParams and axios.get', () => { - const request = RepoService.getContent(url); - - expect(RepoService.buildParams).toHaveBeenCalledWith(url); - expect(axios.get).toHaveBeenCalledWith(url, { - params, - }); - expect(request).toBe(requestPromise); - }); - - it('uses object url prop if no url arg is provided', () => { - RepoService.url = url; - - RepoService.getContent(); - - expect(axios.get).toHaveBeenCalledWith(url, { - params, - }); - }); - }); - - describe('getBase64Content', () => { - const url = 'url'; - const response = { data: 'data' }; - - beforeEach(() => { - spyOn(RepoService, 'bufferToBase64'); - spyOn(axios, 'get').and.returnValue(Promise.resolve(response)); - }); - - it('calls axios.get and bufferToBase64 on completion', (done) => { - const request = RepoService.getBase64Content(url); - - expect(axios.get).toHaveBeenCalledWith(url, { - responseType: 'arraybuffer', - }); - expect(request).toEqual(jasmine.any(Promise)); - - request.then(() => { - expect(RepoService.bufferToBase64).toHaveBeenCalledWith(response.data); - done(); - }).catch(done.fail); - }); - }); - - describe('commitFiles', () => { - it('calls commitMultiple and .then commitFlash', (done) => { - const projectId = 'projectId'; - const payload = {}; - RepoStore.projectId = projectId; - - spyOn(Api, 'commitMultiple').and.returnValue(Promise.resolve()); - spyOn(RepoService, 'commitFlash'); - - const apiPromise = RepoService.commitFiles(payload); - - expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, payload); - - apiPromise.then(() => { - expect(RepoService.commitFlash).toHaveBeenCalled(); - done(); - }).catch(done.fail); - }); - }); - - describe('commitFlash', () => { - it('calls Flash with data.message', () => { - const data = { - message: 'message', - }; - spyOn(window, 'Flash'); - - RepoService.commitFlash(data); - - expect(window.Flash).toHaveBeenCalledWith(data.message); - }); - - it('calls Flash with success string if short_id and stats', () => { - const data = { - short_id: 'short_id', - stats: { - additions: '4', - deletions: '5', - }, - }; - spyOn(window, 'Flash'); - - RepoService.commitFlash(data); - - expect(window.Flash).toHaveBeenCalledWith(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); - }); - }); -}); |