Add create merge checkbox.
This commit is contained in:
parent
3555252d80
commit
c3195e83a8
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Repo Editor: Add option to start a new MR directly from comit section'
|
||||
merge_request: 14665
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default (time = 0) => new Promise((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
|
|
@ -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,56 +92,105 @@ 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);
|
||||
beforeEach((done) => {
|
||||
RepoStore.isCommitable = true;
|
||||
RepoStore.currentBranch = branch;
|
||||
RepoStore.targetBranch = branch;
|
||||
RepoStore.openedFiles = openedFiles;
|
||||
RepoStore.projectId = projectId;
|
||||
|
||||
const vm = createComponent(el);
|
||||
const commitMessageEl = vm.$el.querySelector('#commit-message');
|
||||
const submitCommit = vm.$refs.submitCommit;
|
||||
// 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.commitMessage = commitMessage;
|
||||
vm = createComponent(el);
|
||||
vm.commitMessage = commitMessage;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(commitMessageEl.value).toBe(commitMessage);
|
||||
expect(submitCommit.disabled).toBeFalsy();
|
||||
|
||||
spyOn(vm, 'makeCommit').and.callThrough();
|
||||
spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve());
|
||||
|
||||
submitCommit.click();
|
||||
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(() => {
|
||||
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);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
el.remove();
|
||||
});
|
||||
|
||||
it('shows commit message', () => {
|
||||
const commitMessageEl = vm.$el.querySelector('#commit-message');
|
||||
expect(commitMessageEl.value).toBe(commitMessage);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
|
|
@ -143,6 +199,7 @@ describe('RepoCommitSection', () => {
|
|||
const vm = {
|
||||
submitCommitsLoading: true,
|
||||
changedFiles: new Array(10),
|
||||
openedFiles: new Array(3),
|
||||
commitMessage: 'commitMessage',
|
||||
editMode: true,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue