Commit 34fd07f6 authored by Clement Ho's avatar Clement Ho

Merge branch 'new-mr-repo-editor' into 'master'

Add create merge checkbox.

Closes #38626

See merge request gitlab-org/gitlab-ce!14665
parents 3555252d c3195e83
......@@ -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)
......
......@@ -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;
}
}
......
......@@ -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>
......
......@@ -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) {
......
......@@ -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);
......
......@@ -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;
......
......@@ -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>
......
......@@ -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 } }
---
title: 'Repo Editor: Add option to start a new MR directly from comit section'
merge_request: 14665
author:
type: added
export default (time = 0) => new Promise((resolve) => {
setTimeout(resolve, time);
});
......@@ -2,11 +2,25 @@ 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 = [{
let changedFiles;
let openedFiles;
RepoStore.projectUrl = projectUrl;
function createComponent(el) {
const RepoCommitSection = Vue.extend(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`,
......@@ -19,20 +33,13 @@ describe('RepoCommitSection', () => {
path: 'dir/file1.ext',
newContent: 'b',
}];
const openedFiles = changedFiles.concat([{
openedFiles = changedFiles.concat([{
id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext',
changed: false,
}]);
RepoStore.projectUrl = projectUrl;
function createComponent(el) {
const RepoCommitSection = Vue.extend(repoCommitSection);
return new RepoCommitSection().$mount(el);
}
});
it('renders a commit section', () => {
RepoStore.isCommitable = true;
......@@ -85,9 +92,13 @@ 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';
beforeEach((done) => {
RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch;
......@@ -97,27 +108,55 @@ describe('RepoCommitSection', () => {
// 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');
el = document.createElement('div');
document.body.appendChild(el);
const vm = createComponent(el);
const commitMessageEl = vm.$el.querySelector('#commit-message');
const submitCommit = vm.$refs.submitCommit;
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();
});
});
afterEach(() => {
vm.$destroy();
el.remove();
});
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();
// 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];
......@@ -131,9 +170,26 @@ describe('RepoCommitSection', () => {
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);
done();
})
.then(done)
.catch(done.fail);
});
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,
};
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment