diff options
-rw-r--r-- | app/assets/javascripts/api.js | 14 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/repo/components/repo_commit_section.vue | 91 | ||||
-rw-r--r-- | app/assets/javascripts/repo/index.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/repo/services/repo_service.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/repo/stores/repo_store.js | 18 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/popup_dialog.vue | 13 | ||||
-rw-r--r-- | app/views/shared/repo/_repo.html.haml | 2 | ||||
-rw-r--r-- | changelogs/unreleased/new-mr-repo-editor.yml | 5 | ||||
-rw-r--r-- | spec/javascripts/helpers/set_timeout_promise_helper.js | 3 | ||||
-rw-r--r-- | spec/javascripts/repo/components/repo_commit_section_spec.js | 171 |
11 files changed, 260 insertions, 68 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 38d1effc77c..242b3e2b990 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,6 +15,7 @@ const Api = { issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', usersPath: '/api/:version/users.json', commitPath: '/api/:version/projects/:id/repository/commits', + branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath) @@ -123,6 +124,19 @@ const Api = { }); }, + branchSingle(id, branch) { + const url = Api.buildUrl(Api.branchSinglePath) + .replace(':id', id) + .replace(':branch', branch); + + return this.wrapAjaxCall({ + url, + type: 'GET', + contentType: 'application/json; charset=utf-8', + dataType: 'json', + }); + }, + // Return text for a specific license licenseText(key, data, callback) { const url = Api.buildUrl(Api.licensePath) diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 78c7a094127..1aa63216baf 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) { return hashIndex === -1 ? null : url.substring(hashIndex + 1); }; -w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); +w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href); // eslint-disable-next-line import/prefer-default-export export function visitUrl(url, external = false) { @@ -96,7 +96,7 @@ export function visitUrl(url, external = false) { otherWindow.opener = null; otherWindow.location = url; } else { - document.location.href = url; + window.location.href = url; } } diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 6d8cc964eb2..c0dc4c8cd8b 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -3,11 +3,17 @@ import Flash from '../../flash'; import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; import Service from '../services/repo_service'; +import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import { visitUrl } from '../../lib/utils/url_utility'; export default { + mixins: [RepoMixin], + data: () => Store, - mixins: [RepoMixin], + components: { + PopupDialog, + }, computed: { showCommitable() { @@ -28,7 +34,16 @@ export default { }, methods: { - makeCommit() { + commitToNewBranch(status) { + if (status) { + this.showNewBranchDialog = false; + this.tryCommit(null, true, true); + } else { + // reset the state + } + }, + + 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 => ({ @@ -36,19 +51,63 @@ export default { file_path: f.path, content: f.newContent, })); + const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch; const payload = { - branch: Store.currentBranch, + branch, commit_message: commitMessage, actions, }; - Store.submitCommitsLoading = true; + if (newBranch) { + payload.start_branch = this.currentBranch; + } + this.submitCommitsLoading = true; Service.commitFiles(payload) - .then(this.resetCommitState) - .catch(() => Flash('An error occurred while committing your changes')); + .then(() => { + this.resetCommitState(); + if (this.startNewMR) { + this.redirectToNewMr(branch); + } else { + this.redirectToBranch(branch); + } + }) + .catch(() => { + Flash('An error occurred while committing your changes'); + }); + }, + + tryCommit(e, skipBranchCheck = false, newBranch = false) { + if (skipBranchCheck) { + this.makeCommit(newBranch); + } else { + Store.setBranchHash() + .then(() => { + if (Store.branchChanged) { + Store.showNewBranchDialog = true; + return; + } + this.makeCommit(newBranch); + }) + .catch(() => { + 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; @@ -62,9 +121,17 @@ export default { <div v-if="showCommitable" 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" + /> <form class="form-horizontal" - @submit.prevent="makeCommit"> + @submit.prevent="tryCommit"> <fieldset> <div class="form-group"> <label class="col-md-4 control-label staged-files"> @@ -117,7 +184,7 @@ export default { class="btn btn-success"> <i v-if="submitCommitsLoading" - class="fa fa-spinner fa-spin" + class="js-commit-loading-icon fa fa-spinner fa-spin" aria-hidden="true" aria-label="loading"> </i> @@ -126,6 +193,14 @@ export default { </span> </button> </div> + <div class="col-md-offset-4 col-md-6"> + <div class="checkbox"> + <label> + <input type="checkbox" v-model="startNewMR"> + <span>Start a <strong>new merge request</strong> with these changes</span> + </label> + </div> + </div> </fieldset> </form> </div> diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 7d0123e3d3a..1a09f411b22 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -31,8 +31,11 @@ function setInitialStore(data) { Store.projectUrl = data.projectUrl; Store.canCommit = data.canCommit; Store.onTopOfBranch = data.onTopOfBranch; + Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl); + Store.customBranchURL = decodeURIComponent(data.blobUrl); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.checkIsCommitable(); + Store.setBranchHash(); } function initRepo(el) { diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 830685f7e6e..d68d71a4629 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -64,6 +64,10 @@ const RepoService = { return urlArray.join('/'); }, + getBranch() { + return Api.branchSingle(Store.projectId, Store.currentBranch); + }, + commitFiles(payload) { return Api.commitMultiple(Store.projectId, payload) .then(this.commitFlash); diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index c633f538c1b..f8d29af7ffe 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -23,6 +23,7 @@ const RepoStore = { title: '', status: false, }, + showNewBranchDialog: false, activeFile: Helper.getDefaultActiveFile(), activeFileIndex: 0, activeLine: -1, @@ -31,6 +32,12 @@ const RepoStore = { isCommitable: false, binary: false, currentBranch: '', + startNewMR: false, + currentHash: '', + currentShortHash: '', + customBranchURL: '', + newMrTemplateUrl: '', + branchChanged: false, commitMessage: '', binaryTypes: { png: false, @@ -49,6 +56,17 @@ const RepoStore = { }); }, + 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; diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 9279b50cd55..7d8c5936b7d 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -16,6 +16,11 @@ export default { required: false, default: 'primary', }, + closeKind: { + type: String, + required: false, + default: 'default', + }, closeButtonLabel: { type: String, required: false, @@ -33,6 +38,11 @@ export default { [`btn-${this.kind}`]: true, }; }, + btnCancelKindClass() { + return { + [`btn-${this.closeKind}`]: true, + }; + }, }, methods: { @@ -70,7 +80,8 @@ export default { <div class="modal-footer"> <button type="button" - class="btn btn-default" + class="btn" + :class="btnCancelKindClass" @click="emitSubmit(false)"> {{closeButtonLabel}} </button> diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 87fa2007d16..919f19f2c23 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -3,5 +3,7 @@ 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}}' }), can_commit: (!!can_push_branch?(project, @ref)).to_s, on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } diff --git a/changelogs/unreleased/new-mr-repo-editor.yml b/changelogs/unreleased/new-mr-repo-editor.yml new file mode 100644 index 00000000000..a6c15ee30a9 --- /dev/null +++ b/changelogs/unreleased/new-mr-repo-editor.yml @@ -0,0 +1,5 @@ +--- +title: 'Repo Editor: Add option to start a new MR directly from comit section' +merge_request: 14665 +author: +type: added diff --git a/spec/javascripts/helpers/set_timeout_promise_helper.js b/spec/javascripts/helpers/set_timeout_promise_helper.js new file mode 100644 index 00000000000..1478073413c --- /dev/null +++ b/spec/javascripts/helpers/set_timeout_promise_helper.js @@ -0,0 +1,3 @@ +export default (time = 0) => new Promise((resolve) => { + setTimeout(resolve, time); +}); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index e604dcc152d..0635de4b30b 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -2,29 +2,13 @@ import Vue from 'vue'; 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'; describe('RepoCommitSection', () => { const branch = 'master'; const projectUrl = 'projectUrl'; - const changedFiles = [{ - id: 0, - changed: true, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, - path: 'dir/file0.ext', - newContent: 'a', - }, { - id: 1, - changed: true, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`, - path: 'dir/file1.ext', - newContent: 'b', - }]; - const openedFiles = changedFiles.concat([{ - id: 2, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, - path: 'dir/file2.ext', - changed: false, - }]); + let changedFiles; + let openedFiles; RepoStore.projectUrl = projectUrl; @@ -34,6 +18,29 @@ describe('RepoCommitSection', () => { return new RepoCommitSection().$mount(el); } + 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, + }]); + }); + it('renders a commit section', () => { RepoStore.isCommitable = true; RepoStore.currentBranch = branch; @@ -85,55 +92,104 @@ describe('RepoCommitSection', () => { expect(vm.$el.innerHTML).toBeFalsy(); }); - it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => { + describe('when submitting', () => { + let el; + let vm; const projectId = 'projectId'; const commitMessage = 'commitMessage'; - 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 - const el = document.createElement('div'); - document.body.appendChild(el); - - const vm = createComponent(el); - const commitMessageEl = vm.$el.querySelector('#commit-message'); - const submitCommit = vm.$refs.submitCommit; + 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(); + }); + }); - vm.commitMessage = commitMessage; + afterEach(() => { + vm.$destroy(); + el.remove(); + }); - Vue.nextTick(() => { + it('shows commit message', () => { + const commitMessageEl = vm.$el.querySelector('#commit-message'); expect(commitMessageEl.value).toBe(commitMessage); - expect(submitCommit.disabled).toBeFalsy(); + }); - spyOn(vm, 'makeCommit').and.callThrough(); - spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve()); + it('allows you to submit', () => { + const submitCommit = vm.$refs.submitCommit; + expect(submitCommit.disabled).toBeFalsy(); + }); + it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => { + const submitCommit = vm.$refs.submitCommit; submitCommit.click(); - Vue.nextTick(() => { - expect(vm.makeCommit).toHaveBeenCalled(); - expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy(); - - const args = RepoService.commitFiles.calls.allArgs()[0]; - const { commit_message, actions, branch: payloadBranch } = args[0]; - - expect(commit_message).toBe(commitMessage); - expect(actions.length).toEqual(2); - expect(payloadBranch).toEqual(branch); - 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); + // 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]; + + expect(commit_message).toBe(commitMessage); + expect(actions.length).toEqual(2); + expect(payloadBranch).toEqual(branch); + 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); + }) + .then(done) + .catch(done.fail); + }); - done(); - }); + it('redirects to MR creation page if start new MR checkbox checked', (done) => { + vm.startNewMR = true; + + Vue.nextTick() + .then(() => { + const submitCommit = vm.$refs.submitCommit; + submitCommit.click(); + }) + // Wait for the branch check to finish + .then(() => getSetTimeoutPromise()) + .then(() => { + expect(vm.redirectToNewMr).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); }); @@ -143,6 +199,7 @@ describe('RepoCommitSection', () => { const vm = { submitCommitsLoading: true, changedFiles: new Array(10), + openedFiles: new Array(3), commitMessage: 'commitMessage', editMode: true, }; |