Skip to content
Snippets Groups Projects

Resolve "Customize branch name when using create branch in an issue"

Compare and Show latest version
3 files
+ 273
248
Compare changes
  • Side-by-side
  • Inline
Files
3
@@ -19,8 +19,8 @@ export default class CreateMergeRequestDropdown {
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('.ref');
this.refMessage = this.wrapperEl.querySelector('.ref-message');
this.refInput = this.wrapperEl.querySelector('.js-ref');
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
@@ -44,60 +44,18 @@ export default class CreateMergeRequestDropdown {
this.init();
}
init() {
this.checkAbilityToCreateBranch();
}
available() {
this.availableButton.classList.remove('hide');
this.unavailableButton.classList.add('hide');
}
unavailable() {
this.availableButton.classList.add('hide');
this.unavailableButton.classList.remove('hide');
}
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');
}
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');
}
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';
}
bindEvents() {
this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
// this.refInput.addEventListener('keydown', this.processTab.bind(this));
}
checkAbilityToCreateBranch() {
@@ -129,48 +87,66 @@ export default class CreateMergeRequestDropdown {
});
}
findByValue(objects, ref, exactSearch = true) {
if (!objects || !objects.length) return false;
if (objects.indexOf(ref) > 0) return ref;
if (exactSearch) return false;
return objects[0];
createBranch() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createBranchPath,
beforeSend: () => (this.isCreatingBranch = true),
})
.done((data) => {
this.branchCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
}
getRef(ref, type = 'all') {
if (!ref) return false;
$.ajax({
method: 'GET',
createMergeRequest() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.refsPath + ref,
beforeSend: () => {
this.isGettingRef = true;
},
})
.always(() => {
this.isGettingRef = false;
url: this.createMrPath,
beforeSend: () => (this.isCreatingMergeRequest = true),
})
.done((data) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
if (type === 'branch') return this.findByValue(branches, ref);
return this.findByValue(branches, ref) || this.findByValue(tags, ref);
this.mergeRequestCreated = true;
window.location.href = data.url;
})
.fail(() => {
this.unavailable();
this.disable();
new Flash('Failed to get ref.');
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
}
return false;
});
disable() {
this.disableCreateAction();
this.dropdownToggle.classList.add('disabled');
this.dropdownToggle.setAttribute('disabled', 'disabled');
}
initDroplab() {
this.droplab = new DropLab();
disableCreateAction() {
this.createMergeRequestButton.classList.add('disabled');
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
this.getDroplabConfig());
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');
}
findByValue(objects, ref, returnFirstMatch = false) {
if (!objects || !objects.length) return false;
if (objects.indexOf(ref) > -1) return ref;
if (returnFirstMatch) return objects[0];
return false;
}
getDroplabConfig() {
@@ -198,178 +174,112 @@ export default class CreateMergeRequestDropdown {
};
}
bindEvents() {
this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
this.branchInput.addEventListener('keyup', this.onChangeBranchInput.bind(this));
this.refInput.addEventListener('keyup', this.onChangeRefInput.bind(this));
this.refInput.addEventListener('keydown', this.processTab.bind(this));
}
isBusy() {
return this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
this.branchCreated ||
this.isGettingRef;
}
// target: 'branch', 'ref'
// type: 'checking', 'available', 'not_available'
showMessage(target, type) {
let input;
let message;
let text;
if (target === 'branch') {
input = this.branchInput;
message = this.branchMessage;
text = 'branch name';
} else {
input = this.refInput;
message = this.refMessage;
text = 'source';
}
// Remove gl-field error classes
input.classList.remove('gl-field-error-outline');
input.classList.remove('gl-field-success-outline');
message.classList.remove('gl-field-hint');
message.classList.remove('gl-field-error-message');
message.classList.remove('gl-field-success-message');
if (type === 'checking') {
message.classList.add('gl-field-hint');
message.textContent = `Checking ${text} availability...`;
message.classList.remove('hide');
getRef(ref, target = 'all') {
if (!ref) return false;
return;
}
return $.ajax({
method: 'GET',
dataType: 'json',
url: this.refsPath + ref,
beforeSend: () => {
this.isGettingRef = true;
},
})
.always(() => {
this.isGettingRef = false;
})
.done((data) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
let result;
if (type === 'not_available') {
if (target === 'branch') {
text = 'Branch is already taken';
result = this.findByValue(branches, ref);
} else {
text = 'Source is not available';
result = this.findByValue(branches, ref, true) || this.findByValue(tags, ref, true);
}
input.classList.add('gl-field-error-outline');
message.classList.add('gl-field-error-message');
message.textContent = text;
message.classList.remove('hide');
return;
}
if (type === 'available') {
text = text.charAt(0).toUpperCase() + text.slice(1);
return this.updateInputState(target, ref, result);
})
.fail(() => {
this.unavailable();
this.disable();
new Flash('Failed to get ref.');
input.classList.add('gl-field-success-outline');
message.classList.add('gl-field-success-message');
message.textContent = `${text} is available`;
message.classList.remove('hide');
}
return false;
});
}
onClickSetFocusOnBranchNameInput() {
this.branchInput.focus();
hide() {
this.wrapperEl.classList.add('hide');
}
onChangeBranchInput(event) {
const branch = this.branchInput.value;
if (this.isGettingRef) return;
init() {
this.checkAbilityToCreateBranch();
}
// `ENTER` key submits the data.
if (event.keyCode === 13 && this.inputsAreValid) {
this.createMergeRequestButton.click();
return;
}
initDroplab() {
this.droplab = new DropLab();
// `ESC` key closes the dropdown.
if (event.keyCode === 27) {
this.dropdownToggle.click();
return;
}
this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
this.getDroplabConfig());
}
// If the input is empty, use the original branch name generated by the backend.
if (!branch) {
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
this.createMrPath = this.wrapperEl.dataset.createMrPath;
this.inputsAreValid = true;
this.enable();
this.showMessage('branch', 'available');
return;
}
isBusy() {
return this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
this.branchCreated ||
this.isGettingRef;
}
// If a found branch equals exact the same as a user typed,
// that means a new branch cannot be created as it is already exists.
if (this.getRef(branch, 'branch') === branch) {
this.inputsAreValid = false;
this.disableCreateAction();
this.showMessage('branch', 'not_available');
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 {
this.inputsAreValid = true;
this.enable();
this.showMessage('branch', 'available');
this.createBranchPath = this.createBranchPath.replace(/(branch_name=)(.+?)(?=&issue)/, `$1${branch}`);
this.createMrPath = this.createMrPath.replace(/(branch_name=)(.+?)(?=&ref)/, `$1${branch}`);
return false;
}
}
onChangeRefInput(event) {
// Remove selected text (autocomplete text).
const ref = this.refInput.value.slice(0, this.refInput.selectionStart) +
this.refInput.value.slice(this.refInput.selectionEnd);
if (this.isGettingRef) return;
if (this.isGettingRef) return false;
// `ENTER` key submits the data.
if (event.keyCode === 13 && this.inputsAreValid) {
this.createMergeRequestButton.click();
return;
return true;
}
// `ESC` key closes the dropdown.
if (event.keyCode === 27) {
this.dropdownToggle.click();
return;
return true;
}
// If the input is empty, use the original ref name generated by the backend.
if (!ref) {
// 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;
this.inputsAreValid = true;
this.enable();
this.showMessage('ref', 'available');
return;
this.showAvailableMessage(target);
return true;
}
this.createBranchPath = this.createBranchPath.replace(/(ref=)(.+?)$/, `$1${ref}`);
this.createMrPath = this.createMrPath.replace(/(ref=)(.+?)$/, `$1${ref}`);
this.showCheckingMessage(target);
clearTimeout(this.getRefDelay);
const foundRef = this.getRef(ref, 'all');
this.getRefDelay = setTimeout(() => {
this.getRef(value, target);
}, 500);
if (ref === foundRef) {
this.inputsAreValid = true;
this.enable();
this.showMessage('ref', 'available');
} else {
this.inputsAreValid = false;
this.disableCreateAction();
this.showMessage('ref', 'not_available');
// Show the first found ref as a hint.
if (foundRef) {
clearTimeout(this.delay);
this.delay = setTimeout(() => {
this.refInput.value = foundRef;
this.refInput.setSelectionRange(ref.length, foundRef.length);
}, 500);
}
}
return true;
}
onClickCreateMergeRequestButton(e) {
@@ -396,11 +306,15 @@ export default class CreateMergeRequestDropdown {
this.disable();
}
onClickSetFocusOnBranchNameInput() {
this.branchInput.focus();
}
// `TAB` autocompletes the source.
processTab(event) {
if (event.keyCode !== 9) return;
const value = this.refInput.value;
const value = event.srcElement.value;
event.preventDefault();
@@ -408,31 +322,134 @@ export default class CreateMergeRequestDropdown {
this.refInput.value = value;
}
createMergeRequest() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createMrPath,
beforeSend: () => (this.isCreatingMergeRequest = true),
})
.done((data) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
removeMessage(target) {
const input = target === 'branch' ? this.branchInput : this.refInput;
const message = target === 'branch' ? this.branchMessage : this.refMessage;
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); });
}
createBranch() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createBranchPath,
beforeSend: () => (this.isCreatingBranch = true),
})
.done((data) => {
this.branchCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
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';
}
}
showAvailableMessage(target) {
let input;
let message;
let text;
if (target === 'branch') {
input = this.branchInput;
message = this.branchMessage;
text = 'branch name';
} else {
input = this.refInput;
message = this.refMessage;
text = 'source';
}
this.removeMessage(target);
input.classList.add('gl-field-success-outline');
message.classList.add('gl-field-success-message');
message.textContent = `${text} is available`;
message.classList.remove('hide');
}
showCheckingMessage(target) {
let message;
let text;
if (target === 'branch') {
message = this.branchMessage;
text = 'branch name';
} else {
message = this.refMessage;
text = 'source';
}
this.removeMessage(target);
message.classList.add('gl-field-hint');
message.textContent = `Checking ${text} availability...`;
message.classList.remove('hide');
}
showNotAvailableMessage(target) {
let input;
let message;
let text;
if (target === 'branch') {
input = this.branchInput;
message = this.branchMessage;
text = 'Branch is already taken';
} else {
input = this.refInput;
message = this.refMessage;
text = '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.classList.remove('hide');
}
unavailable() {
this.availableButton.classList.add('hide');
this.unavailableButton.classList.remove('hide');
}
updateInputState(target, ref, result) {
// These regexps are used to replace
// a backend generated new branch name and its source (ref)
// with user's inputs.
const regexps = {
branch: {
createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)')
},
ref: {
createBranchPath: new RegExp('(ref=)(.+?)$'),
createMrPath: new RegExp('(ref=)(.+?)$')
},
};
// If a found branch equals exact the same as a user typed,
// that means a new branch cannot be created as it is already exists.
if (ref === result) {
if (target === 'branch') {
this.inputsAreValid = false;
this.disableCreateAction();
this.showNotAvailableMessage('branch');
} else {
this.inputsAreValid = true;
this.enable();
this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(regexps.ref.createBranchPath, `$1${ref}`);
this.createMrPath = this.createMrPath.replace(regexps.ref.createMrPath, `$1${ref}`);
}
} else if (target === 'branch') {
this.inputsAreValid = true;
this.enable();
this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(regexps.branch.createBranchPath, `$1${ref}`);
this.createMrPath = this.createMrPath.replace(regexps.branch.createMrPath, `$1${ref}`);
} else {
this.inputsAreValid = false;
this.disableCreateAction();
this.showNotAvailableMessage('ref');
}
}
}
Loading