Add an ability to use a custom branch name on creation from issues
This commit is contained in:
parent
acae8ddb34
commit
5bc32b65f1
|
@ -2,6 +2,7 @@
|
||||||
import Flash from './flash';
|
import Flash from './flash';
|
||||||
import DropLab from './droplab/drop_lab';
|
import DropLab from './droplab/drop_lab';
|
||||||
import ISetter from './droplab/plugins/input_setter';
|
import ISetter from './droplab/plugins/input_setter';
|
||||||
|
import { __, sprintf } from './locale';
|
||||||
|
|
||||||
// Todo: Remove this when fixing issue in input_setter plugin
|
// Todo: Remove this when fixing issue in input_setter plugin
|
||||||
const InputSetter = Object.assign({}, ISetter);
|
const InputSetter = Object.assign({}, ISetter);
|
||||||
|
@ -12,70 +13,63 @@ const CREATE_BRANCH = 'create-branch';
|
||||||
export default class CreateMergeRequestDropdown {
|
export default class CreateMergeRequestDropdown {
|
||||||
constructor(wrapperEl) {
|
constructor(wrapperEl) {
|
||||||
this.wrapperEl = wrapperEl;
|
this.wrapperEl = wrapperEl;
|
||||||
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
|
|
||||||
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
|
|
||||||
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
|
|
||||||
this.availableButton = this.wrapperEl.querySelector('.available');
|
this.availableButton = this.wrapperEl.querySelector('.available');
|
||||||
|
this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
|
||||||
|
this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
|
||||||
|
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
|
||||||
|
this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
|
||||||
|
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
|
||||||
|
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
|
||||||
|
this.refInput = this.wrapperEl.querySelector('.js-ref');
|
||||||
|
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
|
||||||
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
|
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
|
||||||
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
|
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
|
||||||
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
|
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
|
||||||
|
|
||||||
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
this.branchCreated = false;
|
||||||
|
this.branchIsValid = true;
|
||||||
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
|
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
|
||||||
|
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
||||||
this.createMrPath = this.wrapperEl.dataset.createMrPath;
|
this.createMrPath = this.wrapperEl.dataset.createMrPath;
|
||||||
this.droplabInitialized = false;
|
this.droplabInitialized = false;
|
||||||
this.isCreatingMergeRequest = false;
|
|
||||||
this.mergeRequestCreated = false;
|
|
||||||
this.isCreatingBranch = false;
|
this.isCreatingBranch = false;
|
||||||
this.branchCreated = false;
|
this.isCreatingMergeRequest = false;
|
||||||
|
this.isGettingRef = false;
|
||||||
|
this.mergeRequestCreated = false;
|
||||||
|
this.refDebounce = _.debounce((value, target) => this.getRef(value, target), 500);
|
||||||
|
this.refIsValid = true;
|
||||||
|
this.refsPath = this.wrapperEl.dataset.refsPath;
|
||||||
|
this.suggestedRef = this.refInput.value;
|
||||||
|
|
||||||
|
// These regexps are used to replace
|
||||||
|
// a backend generated new branch name and its source (ref)
|
||||||
|
// with user's inputs.
|
||||||
|
this.regexps = {
|
||||||
|
branch: {
|
||||||
|
createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
|
||||||
|
createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)'),
|
||||||
|
},
|
||||||
|
ref: {
|
||||||
|
createBranchPath: new RegExp('(ref=)(.+?)$'),
|
||||||
|
createMrPath: new RegExp('(ref=)(.+?)$'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
|
||||||
this.checkAbilityToCreateBranch();
|
|
||||||
}
|
|
||||||
|
|
||||||
available() {
|
available() {
|
||||||
this.availableButton.classList.remove('hide');
|
this.availableButton.classList.remove('hide');
|
||||||
this.unavailableButton.classList.add('hide');
|
this.unavailableButton.classList.add('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
unavailable() {
|
bindEvents() {
|
||||||
this.availableButton.classList.add('hide');
|
this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
|
||||||
this.unavailableButton.classList.remove('hide');
|
this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
|
||||||
}
|
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
|
||||||
|
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
|
||||||
enable() {
|
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
|
||||||
this.createMergeRequestButton.classList.remove('disabled');
|
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
|
||||||
this.createMergeRequestButton.removeAttribute('disabled');
|
|
||||||
|
|
||||||
this.dropdownToggle.classList.remove('disabled');
|
|
||||||
this.dropdownToggle.removeAttribute('disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
this.createMergeRequestButton.classList.add('disabled');
|
|
||||||
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
|
|
||||||
|
|
||||||
this.dropdownToggle.classList.add('disabled');
|
|
||||||
this.dropdownToggle.setAttribute('disabled', 'disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.wrapperEl.classList.add('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
setUnavailableButtonState(isLoading = true) {
|
|
||||||
if (isLoading) {
|
|
||||||
this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
|
|
||||||
this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
|
|
||||||
this.unavailableButtonText.textContent = 'Checking branch availability…';
|
|
||||||
} else {
|
|
||||||
this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
|
|
||||||
this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
|
|
||||||
this.unavailableButtonText.textContent = 'New branch unavailable';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAbilityToCreateBranch() {
|
checkAbilityToCreateBranch() {
|
||||||
|
@ -107,60 +101,18 @@ export default class CreateMergeRequestDropdown {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initDroplab() {
|
createBranch() {
|
||||||
this.droplab = new DropLab();
|
return $.ajax({
|
||||||
|
method: 'POST',
|
||||||
this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
|
dataType: 'json',
|
||||||
this.getDroplabConfig());
|
url: this.createBranchPath,
|
||||||
}
|
beforeSend: () => (this.isCreatingBranch = true),
|
||||||
|
})
|
||||||
getDroplabConfig() {
|
.done((data) => {
|
||||||
return {
|
this.branchCreated = true;
|
||||||
InputSetter: [{
|
window.location.href = data.url;
|
||||||
input: this.createMergeRequestButton,
|
})
|
||||||
valueAttribute: 'data-value',
|
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
|
||||||
inputAttribute: 'data-action',
|
|
||||||
}, {
|
|
||||||
input: this.createMergeRequestButton,
|
|
||||||
valueAttribute: 'data-text',
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
this.createMergeRequestButton
|
|
||||||
.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
isBusy() {
|
|
||||||
return this.isCreatingMergeRequest ||
|
|
||||||
this.mergeRequestCreated ||
|
|
||||||
this.isCreatingBranch ||
|
|
||||||
this.branchCreated;
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickCreateMergeRequestButton(e) {
|
|
||||||
let xhr = null;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (this.isBusy()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
|
|
||||||
xhr = this.createMergeRequest();
|
|
||||||
} else if (e.target.dataset.action === CREATE_BRANCH) {
|
|
||||||
xhr = this.createBranch();
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.fail(() => {
|
|
||||||
this.isCreatingMergeRequest = false;
|
|
||||||
this.isCreatingBranch = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.always(() => this.enable());
|
|
||||||
|
|
||||||
this.disable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createMergeRequest() {
|
createMergeRequest() {
|
||||||
|
@ -177,17 +129,343 @@ export default class CreateMergeRequestDropdown {
|
||||||
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
|
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
createBranch() {
|
disable() {
|
||||||
|
this.disableCreateAction();
|
||||||
|
|
||||||
|
this.dropdownToggle.classList.add('disabled');
|
||||||
|
this.dropdownToggle.setAttribute('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
disableCreateAction() {
|
||||||
|
this.createMergeRequestButton.classList.add('disabled');
|
||||||
|
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
|
||||||
|
|
||||||
|
this.createTargetButton.classList.add('disabled');
|
||||||
|
this.createTargetButton.setAttribute('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this.createMergeRequestButton.classList.remove('disabled');
|
||||||
|
this.createMergeRequestButton.removeAttribute('disabled');
|
||||||
|
|
||||||
|
this.createTargetButton.classList.remove('disabled');
|
||||||
|
this.createTargetButton.removeAttribute('disabled');
|
||||||
|
|
||||||
|
this.dropdownToggle.classList.remove('disabled');
|
||||||
|
this.dropdownToggle.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
static findByValue(objects, ref, returnFirstMatch = false) {
|
||||||
|
if (!objects || !objects.length) return false;
|
||||||
|
if (objects.indexOf(ref) > -1) return ref;
|
||||||
|
if (returnFirstMatch) return objects.find(item => new RegExp(`^${ref}`).test(item));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDroplabConfig() {
|
||||||
|
return {
|
||||||
|
addActiveClassToDropdownButton: true,
|
||||||
|
InputSetter: [
|
||||||
|
{
|
||||||
|
input: this.createMergeRequestButton,
|
||||||
|
valueAttribute: 'data-value',
|
||||||
|
inputAttribute: 'data-action',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: this.createMergeRequestButton,
|
||||||
|
valueAttribute: 'data-text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: this.createTargetButton,
|
||||||
|
valueAttribute: 'data-value',
|
||||||
|
inputAttribute: 'data-action',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: this.createTargetButton,
|
||||||
|
valueAttribute: 'data-text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInputSelectedText(input) {
|
||||||
|
const start = input.selectionStart;
|
||||||
|
const end = input.selectionEnd;
|
||||||
|
|
||||||
|
return input.value.substr(start, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRef(ref, target = 'all') {
|
||||||
|
if (!ref) return false;
|
||||||
|
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
method: 'POST',
|
method: 'GET',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
url: this.createBranchPath,
|
url: this.refsPath + ref,
|
||||||
beforeSend: () => (this.isCreatingBranch = true),
|
beforeSend: () => {
|
||||||
|
this.isGettingRef = true;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
this.isGettingRef = false;
|
||||||
})
|
})
|
||||||
.done((data) => {
|
.done((data) => {
|
||||||
this.branchCreated = true;
|
const branches = data[Object.keys(data)[0]];
|
||||||
window.location.href = data.url;
|
const tags = data[Object.keys(data)[1]];
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (target === 'branch') {
|
||||||
|
result = CreateMergeRequestDropdown.findByValue(branches, ref);
|
||||||
|
} else {
|
||||||
|
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
|
||||||
|
CreateMergeRequestDropdown.findByValue(tags, ref, true);
|
||||||
|
this.suggestedRef = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateInputState(target, ref, result);
|
||||||
})
|
})
|
||||||
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
|
.fail(() => {
|
||||||
|
this.unavailable();
|
||||||
|
this.disable();
|
||||||
|
new Flash('Failed to get ref.');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTargetData(target) {
|
||||||
|
return {
|
||||||
|
input: this[`${target}Input`],
|
||||||
|
message: this[`${target}Message`],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.wrapperEl.classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.checkAbilityToCreateBranch();
|
||||||
|
}
|
||||||
|
|
||||||
|
initDroplab() {
|
||||||
|
this.droplab = new DropLab();
|
||||||
|
|
||||||
|
this.droplab.init(
|
||||||
|
this.dropdownToggle,
|
||||||
|
this.dropdownList,
|
||||||
|
[InputSetter],
|
||||||
|
this.getDroplabConfig(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputsAreValid() {
|
||||||
|
return this.branchIsValid && this.refIsValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBusy() {
|
||||||
|
return this.isCreatingMergeRequest ||
|
||||||
|
this.mergeRequestCreated ||
|
||||||
|
this.isCreatingBranch ||
|
||||||
|
this.branchCreated ||
|
||||||
|
this.isGettingRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeInput(event) {
|
||||||
|
let target;
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if (event.srcElement === this.branchInput) {
|
||||||
|
target = 'branch';
|
||||||
|
value = this.branchInput.value;
|
||||||
|
} else if (event.srcElement === this.refInput) {
|
||||||
|
target = 'ref';
|
||||||
|
value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
|
||||||
|
event.srcElement.value.slice(event.srcElement.selectionEnd);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isGettingRef) return false;
|
||||||
|
|
||||||
|
// `ENTER` key submits the data.
|
||||||
|
if (event.keyCode === 13 && this.inputsAreValid()) {
|
||||||
|
event.preventDefault();
|
||||||
|
return this.createMergeRequestButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the input is empty, use the original value generated by the backend.
|
||||||
|
if (!value) {
|
||||||
|
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
||||||
|
this.createMrPath = this.wrapperEl.dataset.createMrPath;
|
||||||
|
|
||||||
|
if (target === 'branch') {
|
||||||
|
this.branchIsValid = true;
|
||||||
|
} else {
|
||||||
|
this.refIsValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enable();
|
||||||
|
this.showAvailableMessage(target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showCheckingMessage(target);
|
||||||
|
this.refDebounce(value, target);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickCreateMergeRequestButton(event) {
|
||||||
|
let xhr = null;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.isBusy()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.dataset.action === CREATE_MERGE_REQUEST) {
|
||||||
|
xhr = this.createMergeRequest();
|
||||||
|
} else if (event.target.dataset.action === CREATE_BRANCH) {
|
||||||
|
xhr = this.createBranch();
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.fail(() => {
|
||||||
|
this.isCreatingMergeRequest = false;
|
||||||
|
this.isCreatingBranch = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.always(() => this.enable());
|
||||||
|
|
||||||
|
this.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickSetFocusOnBranchNameInput() {
|
||||||
|
this.branchInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// `TAB` autocompletes the source.
|
||||||
|
static processTab(event) {
|
||||||
|
if (event.keyCode !== 9 || this.isGettingRef) return;
|
||||||
|
|
||||||
|
const selectedText = CreateMergeRequestDropdown.getInputSelectedText(this.refInput);
|
||||||
|
|
||||||
|
// if nothing selected, we don't need to autocomplete anything. Do the default TAB action.
|
||||||
|
// If a user manually selected text, don't autocomplete anything. Do the default TAB action.
|
||||||
|
if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(target) {
|
||||||
|
const { input, message } = this.getTargetData(target);
|
||||||
|
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
|
||||||
|
const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
|
||||||
|
|
||||||
|
inputClasses.forEach(cssClass => input.classList.remove(cssClass));
|
||||||
|
messageClasses.forEach(cssClass => message.classList.remove(cssClass));
|
||||||
|
message.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
setUnavailableButtonState(isLoading = true) {
|
||||||
|
if (isLoading) {
|
||||||
|
this.unavailableButtonArrow.classList.add('fa-spin');
|
||||||
|
this.unavailableButtonArrow.classList.add('fa-spinner');
|
||||||
|
this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
|
||||||
|
this.unavailableButtonText.textContent = __('Checking branch availability...');
|
||||||
|
} else {
|
||||||
|
this.unavailableButtonArrow.classList.remove('fa-spin');
|
||||||
|
this.unavailableButtonArrow.classList.remove('fa-spinner');
|
||||||
|
this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
|
||||||
|
this.unavailableButtonText.textContent = __('New branch unavailable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAvailableMessage(target) {
|
||||||
|
const { input, message } = this.getTargetData(target);
|
||||||
|
const text = target === 'branch' ? __('Branch name') : __('Source');
|
||||||
|
|
||||||
|
this.removeMessage(target);
|
||||||
|
input.classList.add('gl-field-success-outline');
|
||||||
|
message.classList.add('gl-field-success-message');
|
||||||
|
message.textContent = sprintf(__('%{text} is available'), { text });
|
||||||
|
message.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
showCheckingMessage(target) {
|
||||||
|
const { message } = this.getTargetData(target);
|
||||||
|
const text = target === 'branch' ? __('branch name') : __('source');
|
||||||
|
|
||||||
|
this.removeMessage(target);
|
||||||
|
message.classList.add('gl-field-hint');
|
||||||
|
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
|
||||||
|
message.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotAvailableMessage(target) {
|
||||||
|
const { input, message } = this.getTargetData(target);
|
||||||
|
const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available');
|
||||||
|
|
||||||
|
this.removeMessage(target);
|
||||||
|
input.classList.add('gl-field-error-outline');
|
||||||
|
message.classList.add('gl-field-error-message');
|
||||||
|
message.textContent = text;
|
||||||
|
message.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
unavailable() {
|
||||||
|
this.availableButton.classList.add('hide');
|
||||||
|
this.unavailableButton.classList.remove('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInputState(target, ref, result) {
|
||||||
|
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
|
||||||
|
// ref - string - what a user typed.
|
||||||
|
// result - string - what has been found on backend.
|
||||||
|
|
||||||
|
const pathReplacement = `$1${ref}`;
|
||||||
|
|
||||||
|
// If a found branch equals exact the same text a user typed,
|
||||||
|
// that means a new branch cannot be created as it already exists.
|
||||||
|
if (ref === result) {
|
||||||
|
if (target === 'branch') {
|
||||||
|
this.branchIsValid = false;
|
||||||
|
this.showNotAvailableMessage('branch');
|
||||||
|
} else {
|
||||||
|
this.refIsValid = true;
|
||||||
|
this.refInput.dataset.value = ref;
|
||||||
|
this.showAvailableMessage('ref');
|
||||||
|
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
|
||||||
|
pathReplacement);
|
||||||
|
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
|
||||||
|
pathReplacement);
|
||||||
|
}
|
||||||
|
} else if (target === 'branch') {
|
||||||
|
this.branchIsValid = true;
|
||||||
|
this.showAvailableMessage('branch');
|
||||||
|
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
|
||||||
|
pathReplacement);
|
||||||
|
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
|
||||||
|
pathReplacement);
|
||||||
|
} else {
|
||||||
|
this.refIsValid = false;
|
||||||
|
this.refInput.dataset.value = ref;
|
||||||
|
this.disableCreateAction();
|
||||||
|
this.showNotAvailableMessage('ref');
|
||||||
|
|
||||||
|
// Show ref hint.
|
||||||
|
if (result) {
|
||||||
|
this.refInput.value = result;
|
||||||
|
this.refInput.setSelectionRange(ref.length, result.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inputsAreValid()) {
|
||||||
|
this.enable();
|
||||||
|
} else {
|
||||||
|
this.disableCreateAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
|
||||||
const SELECTED_CLASS = 'droplab-item-selected';
|
const SELECTED_CLASS = 'droplab-item-selected';
|
||||||
const ACTIVE_CLASS = 'droplab-item-active';
|
const ACTIVE_CLASS = 'droplab-item-active';
|
||||||
const IGNORE_CLASS = 'droplab-item-ignore';
|
const IGNORE_CLASS = 'droplab-item-ignore';
|
||||||
|
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
|
||||||
// Matches `{{anything}}` and `{{ everything }}`.
|
// Matches `{{anything}}` and `{{ everything }}`.
|
||||||
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
|
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
|
||||||
|
|
||||||
|
@ -13,4 +14,5 @@ export {
|
||||||
ACTIVE_CLASS,
|
ACTIVE_CLASS,
|
||||||
TEMPLATE_REGEX,
|
TEMPLATE_REGEX,
|
||||||
IGNORE_CLASS,
|
IGNORE_CLASS,
|
||||||
|
IGNORE_HIDING_CLASS,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
|
import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
|
||||||
|
|
||||||
class DropDown {
|
class DropDown {
|
||||||
constructor(list) {
|
constructor(list, config = {}) {
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
this.hidden = true;
|
this.hidden = true;
|
||||||
this.list = typeof list === 'string' ? document.querySelector(list) : list;
|
this.list = typeof list === 'string' ? document.querySelector(list) : list;
|
||||||
this.items = [];
|
this.items = [];
|
||||||
|
|
||||||
this.eventWrapper = {};
|
this.eventWrapper = {};
|
||||||
|
|
||||||
|
if (config.addActiveClassToDropdownButton) {
|
||||||
|
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
this.getItems();
|
this.getItems();
|
||||||
this.initTemplateString();
|
this.initTemplateString();
|
||||||
this.addEvents();
|
this.addEvents();
|
||||||
|
@ -42,7 +45,7 @@ class DropDown {
|
||||||
this.addSelectedClass(selected);
|
this.addSelectedClass(selected);
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.hide();
|
if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
|
||||||
|
|
||||||
const listEvent = new CustomEvent('click.dl', {
|
const listEvent = new CustomEvent('click.dl', {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -67,7 +70,20 @@ class DropDown {
|
||||||
|
|
||||||
addEvents() {
|
addEvents() {
|
||||||
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
|
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
|
||||||
|
this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
|
||||||
|
|
||||||
this.list.addEventListener('click', this.eventWrapper.clickEvent);
|
this.list.addEventListener('click', this.eventWrapper.clickEvent);
|
||||||
|
this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown(event) {
|
||||||
|
// `ESC` key closes the dropdown.
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
event.preventDefault();
|
||||||
|
return this.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
|
@ -110,6 +126,8 @@ class DropDown {
|
||||||
this.list.style.display = 'block';
|
this.list.style.display = 'block';
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
this.hidden = false;
|
this.hidden = false;
|
||||||
|
|
||||||
|
if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
|
@ -117,6 +135,8 @@ class DropDown {
|
||||||
this.list.style.display = 'none';
|
this.list.style.display = 'none';
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
this.hidden = true;
|
this.hidden = true;
|
||||||
|
|
||||||
|
if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
|
@ -128,6 +148,7 @@ class DropDown {
|
||||||
destroy() {
|
destroy() {
|
||||||
this.hide();
|
this.hide();
|
||||||
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
|
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
|
||||||
|
this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
static setImagesSrc(template) {
|
static setImagesSrc(template) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import DropDown from './drop_down';
|
||||||
class Hook {
|
class Hook {
|
||||||
constructor(trigger, list, plugins, config) {
|
constructor(trigger, list, plugins, config) {
|
||||||
this.trigger = trigger;
|
this.trigger = trigger;
|
||||||
this.list = new DropDown(list);
|
this.list = new DropDown(list, config);
|
||||||
this.type = 'Hook';
|
this.type = 'Hook';
|
||||||
this.event = 'click';
|
this.event = 'click';
|
||||||
this.plugins = plugins || [];
|
this.plugins = plugins || [];
|
||||||
|
|
|
@ -203,7 +203,24 @@ ul.related-merge-requests > li {
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-mr-dropdown-wrap {
|
.create-mr-dropdown-wrap {
|
||||||
@include new-style-dropdown;
|
.branch-message,
|
||||||
|
.ref-message {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref::selection {
|
||||||
|
color: $placeholder-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
.dropdown-menu-toggle {
|
||||||
|
min-width: 285px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-select {
|
||||||
|
width: 285px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-group:not(.hide) {
|
.btn-group:not(.hide) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -214,15 +231,16 @@ ul.related-merge-requests > li {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.create-merge-request-dropdown-menu {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
display: none;
|
display: none;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-toggle {
|
.create-merge-request-dropdown-toggle {
|
||||||
.fa-caret-down {
|
.fa-caret-down {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
@ -230,18 +248,50 @@ ul.related-merge-requests > li {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.droplab-item-ignore {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-item {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 1px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: $dropdown-item-hover-bg;
|
||||||
|
color: $gl-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.divider {
|
||||||
|
margin: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
li:not(.divider) {
|
li:not(.divider) {
|
||||||
|
padding: 8px 9px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&.droplab-item-selected {
|
&.droplab-item-selected {
|
||||||
.icon-container {
|
.icon-container {
|
||||||
i {
|
i {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.droplab-item-ignore {
|
||||||
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
float: left;
|
float: left;
|
||||||
padding-left: 6px;
|
|
||||||
|
|
||||||
i {
|
i {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -249,13 +299,12 @@ ul.related-merge-requests > li {
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
padding-left: 30px;
|
padding-left: 22px;
|
||||||
font-size: 13px;
|
}
|
||||||
|
|
||||||
strong {
|
input,
|
||||||
display: block;
|
span {
|
||||||
font-weight: $gl-font-weight-bold;
|
margin: 4px 0 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_merge_request
|
def create_merge_request
|
||||||
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
|
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
|
||||||
|
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
|
||||||
|
|
||||||
if result[:status] == :success
|
if result[:status] == :success
|
||||||
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
|
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
module MergeRequests
|
module MergeRequests
|
||||||
class BuildService < MergeRequests::BaseService
|
class BuildService < MergeRequests::BaseService
|
||||||
def execute
|
def execute
|
||||||
|
@issue_iid = params.delete(:issue_iid)
|
||||||
|
|
||||||
self.merge_request = MergeRequest.new(params)
|
self.merge_request = MergeRequest.new(params)
|
||||||
merge_request.compare_commits = []
|
merge_request.compare_commits = []
|
||||||
merge_request.source_project = find_source_project
|
merge_request.source_project = find_source_project
|
||||||
|
@ -106,37 +108,53 @@ module MergeRequests
|
||||||
# more than one commit in the MR
|
# more than one commit in the MR
|
||||||
#
|
#
|
||||||
def assign_title_and_description
|
def assign_title_and_description
|
||||||
if match = source_branch.match(/\A(\d+)-/)
|
assign_title_and_description_from_single_commit
|
||||||
iid = match[1]
|
assign_title_from_issue
|
||||||
end
|
|
||||||
|
|
||||||
commits = compare_commits
|
merge_request.title ||= source_branch.titleize.humanize
|
||||||
if commits && commits.count == 1
|
merge_request.title = wip_title if compare_commits.empty?
|
||||||
commit = commits.first
|
|
||||||
merge_request.title = commit.title
|
append_closes_description
|
||||||
merge_request.description ||= commit.description.try(:strip)
|
end
|
||||||
elsif iid && issue = target_project.get_issue(iid, current_user)
|
|
||||||
case issue
|
def append_closes_description
|
||||||
when Issue
|
return unless issue_iid
|
||||||
merge_request.title = "Resolve \"#{issue.title}\""
|
|
||||||
when ExternalIssue
|
closes_issue = "Closes ##{issue_iid}"
|
||||||
merge_request.title = "Resolve #{issue.title}"
|
|
||||||
end
|
if description.present?
|
||||||
|
merge_request.description += closes_issue.prepend("\n\n")
|
||||||
else
|
else
|
||||||
merge_request.title = source_branch.titleize.humanize
|
merge_request.description = closes_issue
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if iid
|
def assign_title_and_description_from_single_commit
|
||||||
closes_issue = "Closes ##{iid}"
|
commits = compare_commits
|
||||||
|
|
||||||
if description.present?
|
return unless commits&.count == 1
|
||||||
merge_request.description += closes_issue.prepend("\n\n")
|
|
||||||
else
|
commit = commits.first
|
||||||
merge_request.description = closes_issue
|
merge_request.title ||= commit.title
|
||||||
|
merge_request.description ||= commit.description.try(:strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_title_from_issue
|
||||||
|
return unless issue
|
||||||
|
|
||||||
|
merge_request.title =
|
||||||
|
case issue
|
||||||
|
when Issue then "Resolve \"#{issue.title}\""
|
||||||
|
when ExternalIssue then "Resolve #{issue.title}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
merge_request.title = wip_title if commits.empty?
|
def issue_iid
|
||||||
|
@issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue
|
||||||
|
@issue ||= target_project.get_issue(issue_iid, current_user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
module MergeRequests
|
module MergeRequests
|
||||||
class CreateFromIssueService < MergeRequests::CreateService
|
class CreateFromIssueService < MergeRequests::CreateService
|
||||||
|
def initialize(project, user, params)
|
||||||
|
# branch - the name of new branch
|
||||||
|
# ref - the source of new branch.
|
||||||
|
|
||||||
|
@branch_name = params[:branch_name]
|
||||||
|
@issue_iid = params[:issue_iid]
|
||||||
|
@ref = params[:ref]
|
||||||
|
|
||||||
|
super(project, user)
|
||||||
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
return error('Invalid issue iid') unless issue_iid.present? && issue.present?
|
return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
|
||||||
|
|
||||||
params[:label_ids] = issue.label_ids if issue.label_ids.any?
|
params[:label_ids] = issue.label_ids if issue.label_ids.any?
|
||||||
|
|
||||||
|
@ -21,20 +32,16 @@ module MergeRequests
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def issue_iid
|
|
||||||
@isssue_iid ||= params.delete(:issue_iid)
|
|
||||||
end
|
|
||||||
|
|
||||||
def issue
|
def issue
|
||||||
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
|
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
|
||||||
end
|
end
|
||||||
|
|
||||||
def branch_name
|
def branch_name
|
||||||
@branch_name ||= issue.to_branch_name
|
@branch ||= @branch_name || issue.to_branch_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def ref
|
def ref
|
||||||
project.default_branch || 'master'
|
@ref || project.default_branch || 'master'
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_request
|
def merge_request
|
||||||
|
@ -43,6 +50,7 @@ module MergeRequests
|
||||||
|
|
||||||
def merge_request_params
|
def merge_request_params
|
||||||
{
|
{
|
||||||
|
issue_iid: @issue_iid,
|
||||||
source_project_id: project.id,
|
source_project_id: project.id,
|
||||||
source_branch: branch_name,
|
source_branch: branch_name,
|
||||||
target_project_id: project.id,
|
target_project_id: project.id,
|
||||||
|
|
|
@ -1,34 +1,50 @@
|
||||||
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
|
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
|
||||||
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
|
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
|
||||||
- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch'
|
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
|
||||||
|
|
||||||
- if can?(current_user, :push_code, @project)
|
- if can?(current_user, :push_code, @project)
|
||||||
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
|
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
|
||||||
|
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
|
||||||
|
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
|
||||||
|
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
|
||||||
|
|
||||||
|
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
|
||||||
.btn-group.unavailable
|
.btn-group.unavailable
|
||||||
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
|
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
|
||||||
= icon('spinner', class: 'fa-spin')
|
= icon('spinner', class: 'fa-spin')
|
||||||
%span.text
|
%span.text
|
||||||
Checking branch availability…
|
Checking branch availability…
|
||||||
.btn-group.available.hide
|
.btn-group.available.hide
|
||||||
%input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }
|
%button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
|
||||||
%button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
|
= value
|
||||||
|
|
||||||
|
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
|
||||||
= icon('caret-down')
|
= icon('caret-down')
|
||||||
%ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
|
|
||||||
|
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
|
||||||
- if can_create_merge_request
|
- if can_create_merge_request
|
||||||
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
|
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
|
||||||
.menu-item
|
.menu-item.droplab-item-ignore-hiding
|
||||||
.icon-container
|
.icon-container.droplab-item-ignore-hiding= icon('check')
|
||||||
= icon('check')
|
.description.droplab-item-ignore-hiding Create merge request and branch
|
||||||
.description
|
|
||||||
%strong Create a merge request
|
%li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
|
||||||
%span
|
.menu-item.droplab-item-ignore-hiding
|
||||||
Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
|
.icon-container.droplab-item-ignore-hiding= icon('check')
|
||||||
%li.divider.droplab-item-ignore
|
.description.droplab-item-ignore-hiding Create branch
|
||||||
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
|
%li.divider
|
||||||
.menu-item
|
|
||||||
.icon-container
|
%li.droplab-item-ignore
|
||||||
= icon('check')
|
Branch name
|
||||||
.description
|
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
|
||||||
%strong Create a branch
|
%span.js-branch-message.branch-message.droplab-item-ignore
|
||||||
%span
|
|
||||||
Creates a branch named after this issue, from '#{@project.default_branch}'.
|
%li.droplab-item-ignore
|
||||||
|
Source (branch or tag)
|
||||||
|
%input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
|
||||||
|
%span.js-ref-message.ref-message.droplab-item-ignore
|
||||||
|
|
||||||
|
%li.droplab-item-ignore
|
||||||
|
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
|
||||||
|
Create merge request
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add an ability to use a custom branch name on creation from issues
|
||||||
|
merge_request: 13884
|
||||||
|
author: Vitaliy @blackst0ne Klachkov
|
||||||
|
type: added
|
|
@ -1,106 +0,0 @@
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
feature 'Create Branch/Merge Request Dropdown on issue page', :feature, :js do
|
|
||||||
let(:user) { create(:user) }
|
|
||||||
let!(:project) { create(:project, :repository) }
|
|
||||||
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
|
|
||||||
|
|
||||||
context 'for team members' do
|
|
||||||
before do
|
|
||||||
project.team << [user, :developer]
|
|
||||||
sign_in(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows creating a merge request from the issue page' do
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
|
|
||||||
perform_enqueued_jobs do
|
|
||||||
select_dropdown_option('create-mr')
|
|
||||||
|
|
||||||
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
|
|
||||||
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
|
|
||||||
|
|
||||||
wait_for_requests
|
|
||||||
end
|
|
||||||
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
|
|
||||||
expect(page).to have_content("created branch 1-cherry-coloured-funk")
|
|
||||||
expect(page).to have_content("mentioned in merge request !1")
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows creating a branch from the issue page' do
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
|
|
||||||
select_dropdown_option('create-branch')
|
|
||||||
|
|
||||||
wait_for_requests
|
|
||||||
|
|
||||||
expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
|
|
||||||
expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when there is a referenced merge request" do
|
|
||||||
let!(:note) do
|
|
||||||
create(:note, :on_issue, :system, project: project, noteable: issue,
|
|
||||||
note: "mentioned in #{referenced_mr.to_reference}")
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:referenced_mr) do
|
|
||||||
create(:merge_request, :simple, source_project: project, target_project: project,
|
|
||||||
description: "Fixes #{issue.to_reference}", author: user)
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
referenced_mr.cache_merge_request_closes_issues!(user)
|
|
||||||
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'disables the create branch button' do
|
|
||||||
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
|
|
||||||
expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
|
|
||||||
expect(page).to have_content /1 Related Merge Request/
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when merge requests are disabled' do
|
|
||||||
before do
|
|
||||||
project.project_feature.update(merge_requests_access_level: 0)
|
|
||||||
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows only create branch button' do
|
|
||||||
expect(page).not_to have_button('Create a merge request')
|
|
||||||
expect(page).to have_button('Create a branch')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when issue is confidential' do
|
|
||||||
it 'disables the create branch button' do
|
|
||||||
issue = create(:issue, :confidential, project: project)
|
|
||||||
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
|
|
||||||
expect(page).not_to have_css('.create-mr-dropdown-wrap')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for visitors' do
|
|
||||||
before do
|
|
||||||
visit project_issue_path(project, issue)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows no buttons' do
|
|
||||||
expect(page).not_to have_selector('.create-mr-dropdown-wrap')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_dropdown_option(option)
|
|
||||||
find('.create-mr-dropdown-wrap .dropdown-toggle').click
|
|
||||||
find("li[data-value='#{option}']").click
|
|
||||||
find('.js-create-merge-request').click
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'User creates branch and merge request on issue page', :js do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let!(:project) { create(:project, :repository) }
|
||||||
|
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
|
||||||
|
|
||||||
|
context 'when signed out' do
|
||||||
|
before do
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't show 'Create merge request' button" do
|
||||||
|
expect(page).not_to have_selector('.create-mr-dropdown-wrap')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when signed in' do
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when interacting with the dropdown' do
|
||||||
|
before do
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
# In order to improve tests performance, all UI checks are placed in this test.
|
||||||
|
it 'shows elements' do
|
||||||
|
button_create_merge_request = find('.js-create-merge-request')
|
||||||
|
button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
|
||||||
|
|
||||||
|
button_toggle_dropdown.click
|
||||||
|
|
||||||
|
dropdown = find('.create-merge-request-dropdown-menu')
|
||||||
|
|
||||||
|
page.within(dropdown) do
|
||||||
|
button_create_target = find('.js-create-target')
|
||||||
|
input_branch_name = find('.js-branch-name')
|
||||||
|
input_source = find('.js-ref')
|
||||||
|
li_create_branch = find("li[data-value='create-branch']")
|
||||||
|
li_create_merge_request = find("li[data-value='create-mr']")
|
||||||
|
|
||||||
|
# Test that all elements are presented.
|
||||||
|
expect(page).to have_content('Create merge request and branch')
|
||||||
|
expect(page).to have_content('Create branch')
|
||||||
|
expect(page).to have_content('Branch name')
|
||||||
|
expect(page).to have_content('Source (branch or tag)')
|
||||||
|
expect(page).to have_button('Create merge request')
|
||||||
|
expect(page).to have_selector('.js-branch-name:focus')
|
||||||
|
|
||||||
|
test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
|
||||||
|
test_branch_name_checking(input_branch_name)
|
||||||
|
test_source_checking(input_source)
|
||||||
|
|
||||||
|
# The button inside dropdown should be disabled if any errors occured.
|
||||||
|
expect(page).to have_button('Create branch', disabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# The top level button should be disabled if any errors occured.
|
||||||
|
expect(page).to have_button('Create branch', disabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when branch name is auto-generated' do
|
||||||
|
it 'creates a merge request' do
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
select_dropdown_option('create-mr')
|
||||||
|
|
||||||
|
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
|
||||||
|
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
end
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
|
expect(page).to have_content('created branch 1-cherry-coloured-funk')
|
||||||
|
expect(page).to have_content('mentioned in merge request !1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a branch' do
|
||||||
|
select_dropdown_option('create-branch')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
|
||||||
|
expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when branch name is custom' do
|
||||||
|
let(:branch_name) { 'custom-branch-name' }
|
||||||
|
|
||||||
|
it 'creates a merge request' do
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
select_dropdown_option('create-mr', branch_name)
|
||||||
|
|
||||||
|
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
|
||||||
|
expect(page).to have_content('Request to merge custom-branch-name into')
|
||||||
|
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
end
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
|
expect(page).to have_content('created branch custom-branch-name')
|
||||||
|
expect(page).to have_content('mentioned in merge request !1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a branch' do
|
||||||
|
select_dropdown_option('create-branch', branch_name)
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_selector('.dropdown-toggle-text ', text: branch_name)
|
||||||
|
expect(current_path).to eq project_tree_path(project, branch_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when there is a referenced merge request" do
|
||||||
|
let!(:note) do
|
||||||
|
create(:note, :on_issue, :system, project: project, noteable: issue,
|
||||||
|
note: "mentioned in #{referenced_mr.to_reference}")
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:referenced_mr) do
|
||||||
|
create(:merge_request, :simple, source_project: project, target_project: project,
|
||||||
|
description: "Fixes #{issue.to_reference}", author: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
referenced_mr.cache_merge_request_closes_issues!(user)
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'disables the create branch button' do
|
||||||
|
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
|
||||||
|
expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
|
||||||
|
expect(page).to have_content /1 Related Merge Request/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when merge requests are disabled' do
|
||||||
|
before do
|
||||||
|
project.project_feature.update(merge_requests_access_level: 0)
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows only create branch button' do
|
||||||
|
expect(page).not_to have_button('Create merge request')
|
||||||
|
expect(page).to have_button('Create branch')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issue is confidential' do
|
||||||
|
let(:issue) { create(:issue, :confidential, project: project) }
|
||||||
|
|
||||||
|
it 'disables the create branch button' do
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
|
expect(page).not_to have_css('.create-mr-dropdown-wrap')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def select_dropdown_option(option, branch_name = nil)
|
||||||
|
find('.create-mr-dropdown-wrap .dropdown-toggle').click
|
||||||
|
find("li[data-value='#{option}']").click
|
||||||
|
|
||||||
|
if branch_name
|
||||||
|
find('.js-branch-name').set(branch_name)
|
||||||
|
|
||||||
|
# Javascript debounces AJAX calls.
|
||||||
|
# So we have to wait until AJAX requests are started.
|
||||||
|
# Details are in app/assets/javascripts/create_merge_request_dropdown.js
|
||||||
|
# this.refDebounce = _.debounce(...)
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
end
|
||||||
|
|
||||||
|
find('.js-create-merge-request').click
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_branch_name_checking(input_branch_name)
|
||||||
|
expect(input_branch_name.value).to eq(issue.to_branch_name)
|
||||||
|
|
||||||
|
input_branch_name.set('new-branch-name')
|
||||||
|
branch_name_message = find('.js-branch-message')
|
||||||
|
|
||||||
|
expect(branch_name_message).to have_text('Checking branch name availability…')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(branch_name_message).to have_text('Branch name is available')
|
||||||
|
|
||||||
|
input_branch_name.set(project.default_branch)
|
||||||
|
|
||||||
|
expect(branch_name_message).to have_text('Checking branch name availability…')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(branch_name_message).to have_text('Branch is already taken')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
|
||||||
|
page.within(li_create_merge_request) do
|
||||||
|
expect(page).to have_css('i.fa.fa-check')
|
||||||
|
expect(button_create_target).to have_text('Create merge request')
|
||||||
|
expect(button_create_merge_request).to have_text('Create merge request')
|
||||||
|
end
|
||||||
|
|
||||||
|
li_create_branch.click
|
||||||
|
|
||||||
|
page.within(li_create_branch) do
|
||||||
|
expect(page).to have_css('i.fa.fa-check')
|
||||||
|
expect(button_create_target).to have_text('Create branch')
|
||||||
|
expect(button_create_merge_request).to have_text('Create branch')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_source_checking(input_source)
|
||||||
|
expect(input_source.value).to eq(project.default_branch)
|
||||||
|
|
||||||
|
input_source.set('mas') # Intentionally entered first 3 letters of `master` to check autocomplete feature later.
|
||||||
|
source_message = find('.js-ref-message')
|
||||||
|
|
||||||
|
expect(source_message).to have_text('Checking source availability…')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(source_message).to have_text('Source is not available')
|
||||||
|
|
||||||
|
# JavaScript gets refs started with `mas` (entered above) and places the first match.
|
||||||
|
# User sees `mas` in black color (the part he entered) and the `ter` in gray color (a hint).
|
||||||
|
# Since hinting is implemented via text selection and rspec/capybara doesn't have matchers for it,
|
||||||
|
# we just checking the whole source name.
|
||||||
|
expect(input_source.value).to eq(project.default_branch)
|
||||||
|
end
|
||||||
|
end
|
|
@ -279,7 +279,12 @@ describe('DropDown', function () {
|
||||||
describe('addEvents', function () {
|
describe('addEvents', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.list = { addEventListener: () => {} };
|
this.list = { addEventListener: () => {} };
|
||||||
this.dropdown = { list: this.list, clickEvent: () => {}, eventWrapper: {} };
|
this.dropdown = {
|
||||||
|
list: this.list,
|
||||||
|
clickEvent: () => {},
|
||||||
|
closeDropdown: () => {},
|
||||||
|
eventWrapper: {},
|
||||||
|
};
|
||||||
|
|
||||||
spyOn(this.list, 'addEventListener');
|
spyOn(this.list, 'addEventListener');
|
||||||
|
|
||||||
|
@ -288,6 +293,7 @@ describe('DropDown', function () {
|
||||||
|
|
||||||
it('should call .addEventListener', function () {
|
it('should call .addEventListener', function () {
|
||||||
expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
|
expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
|
||||||
|
expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('Hook', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call DropDown constructor', function () {
|
it('should call DropDown constructor', function () {
|
||||||
expect(dropdownSrc.default).toHaveBeenCalledWith(this.list);
|
expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set .type', function () {
|
it('should set .type', function () {
|
||||||
|
|
|
@ -6,8 +6,10 @@ describe MergeRequests::CreateFromIssueService do
|
||||||
let(:label_ids) { create_pair(:label, project: project).map(&:id) }
|
let(:label_ids) { create_pair(:label, project: project).map(&:id) }
|
||||||
let(:milestone_id) { create(:milestone, project: project).id }
|
let(:milestone_id) { create(:milestone, project: project).id }
|
||||||
let(:issue) { create(:issue, project: project, milestone_id: milestone_id) }
|
let(:issue) { create(:issue, project: project, milestone_id: milestone_id) }
|
||||||
|
let(:custom_source_branch) { 'custom-source-branch' }
|
||||||
|
|
||||||
subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
|
subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
|
||||||
|
subject(:service_with_custom_source_branch) { described_class.new(project, user, issue_iid: issue.iid, branch_name: custom_source_branch) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
|
@ -17,8 +19,8 @@ describe MergeRequests::CreateFromIssueService do
|
||||||
it 'returns an error with invalid issue iid' do
|
it 'returns an error with invalid issue iid' do
|
||||||
result = described_class.new(project, user, issue_iid: -1).execute
|
result = described_class.new(project, user, issue_iid: -1).execute
|
||||||
|
|
||||||
expect(result[:status]).to eq :error
|
expect(result[:status]).to eq(:error)
|
||||||
expect(result[:message]).to eq 'Invalid issue iid'
|
expect(result[:message]).to eq('Invalid issue iid')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'delegates issue search to IssuesFinder' do
|
it 'delegates issue search to IssuesFinder' do
|
||||||
|
@ -53,6 +55,12 @@ describe MergeRequests::CreateFromIssueService do
|
||||||
expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
|
expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a branch using passed name' do
|
||||||
|
service_with_custom_source_branch.execute
|
||||||
|
|
||||||
|
expect(project.repository.branch_exists?(custom_source_branch)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a system note' do
|
it 'creates a system note' do
|
||||||
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
|
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
|
||||||
|
|
||||||
|
@ -72,19 +80,25 @@ describe MergeRequests::CreateFromIssueService do
|
||||||
it 'sets the merge request author to current user' do
|
it 'sets the merge request author to current user' do
|
||||||
result = service.execute
|
result = service.execute
|
||||||
|
|
||||||
expect(result[:merge_request].author).to eq user
|
expect(result[:merge_request].author).to eq(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets the merge request source branch to the new issue branch' do
|
it 'sets the merge request source branch to the new issue branch' do
|
||||||
result = service.execute
|
result = service.execute
|
||||||
|
|
||||||
expect(result[:merge_request].source_branch).to eq issue.to_branch_name
|
expect(result[:merge_request].source_branch).to eq(issue.to_branch_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the merge request source branch to the passed branch name' do
|
||||||
|
result = service_with_custom_source_branch.execute
|
||||||
|
|
||||||
|
expect(result[:merge_request].source_branch).to eq(custom_source_branch)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets the merge request target branch to the project default branch' do
|
it 'sets the merge request target branch to the project default branch' do
|
||||||
result = service.execute
|
result = service.execute
|
||||||
|
|
||||||
expect(result[:merge_request].target_branch).to eq project.default_branch
|
expect(result[:merge_request].target_branch).to eq(project.default_branch)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue