debian-mirror-gitlab/app/assets/javascripts/issues/create_merge_request_dropdown.js

564 lines
17 KiB
JavaScript
Raw Normal View History

2020-04-22 19:07:51 +05:30
import { debounce } from 'lodash';
2019-09-30 21:07:59 +05:30
import {
init as initConfidentialMergeRequest,
isConfidentialIssue,
canCreateConfidentialMergeRequest,
2022-03-02 08:16:31 +05:30
} from '~/confidential_merge_request';
import confidentialMergeRequestState from '~/confidential_merge_request/state';
import DropLab from '~/filtered_search/droplab/drop_lab_deprecated';
import ISetter from '~/filtered_search/droplab/plugins/input_setter';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
2017-08-17 22:00:37 +05:30
// Todo: Remove this when fixing issue in input_setter plugin
2020-05-24 23:13:21 +05:30
const InputSetter = { ...ISetter };
2017-08-17 22:00:37 +05:30
const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch';
2019-09-30 21:07:59 +05:30
function createEndpoint(projectPath, endpoint) {
if (canCreateConfidentialMergeRequest()) {
return endpoint.replace(
projectPath,
confidentialMergeRequestState.selectedProject.pathWithNamespace,
);
}
return endpoint;
}
2017-08-17 22:00:37 +05:30
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
2018-03-17 18:26:18 +05:30
this.availableButton = this.wrapperEl.querySelector('.available');
this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
2017-08-17 22:00:37 +05:30
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
2021-04-29 21:17:54 +05:30
this.createMergeRequestLoading = this.createMergeRequestButton.querySelector('.js-spinner');
2018-03-17 18:26:18 +05:30
this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
2017-08-17 22:00:37 +05:30
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
2018-03-17 18:26:18 +05:30
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
this.refInput = this.wrapperEl.querySelector('.js-ref');
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
2017-08-17 22:00:37 +05:30
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
2021-06-08 01:23:25 +05:30
this.unavailableButtonSpinner = this.unavailableButton.querySelector('.gl-spinner');
2017-08-17 22:00:37 +05:30
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
2018-03-17 18:26:18 +05:30
this.branchCreated = false;
this.branchIsValid = true;
2017-08-17 22:00:37 +05:30
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
2018-03-17 18:26:18 +05:30
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
2017-08-17 22:00:37 +05:30
this.createMrPath = this.wrapperEl.dataset.createMrPath;
this.droplabInitialized = false;
2018-03-17 18:26:18 +05:30
this.isCreatingBranch = false;
2017-08-17 22:00:37 +05:30
this.isCreatingMergeRequest = false;
2018-03-17 18:26:18 +05:30
this.isGettingRef = false;
2021-12-11 22:18:48 +05:30
this.refCancelToken = null;
2017-08-17 22:00:37 +05:30
this.mergeRequestCreated = false;
2020-04-22 19:07:51 +05:30
this.refDebounce = debounce((value, target) => this.getRef(value, target), 500);
2018-03-17 18:26:18 +05:30
this.refIsValid = true;
this.refsPath = this.wrapperEl.dataset.refsPath;
this.suggestedRef = this.refInput.value;
2019-09-30 21:07:59 +05:30
this.projectPath = this.wrapperEl.dataset.projectPath;
this.projectId = this.wrapperEl.dataset.projectId;
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
// 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)'),
2022-04-04 11:22:00 +05:30
createMrPath: new RegExp('(source_branch%5D=)(.+?)(?=&)'),
2018-03-17 18:26:18 +05:30
},
ref: {
createBranchPath: new RegExp('(ref=)(.+?)$'),
2022-04-04 11:22:00 +05:30
createMrPath: new RegExp('(target_branch%5D=)(.+?)$'),
2018-03-17 18:26:18 +05:30
},
};
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
this.init();
2019-09-30 21:07:59 +05:30
if (isConfidentialIssue()) {
this.createMergeRequestButton.setAttribute(
'data-dropdown-trigger',
'#create-merge-request-dropdown',
);
initConfidentialMergeRequest();
}
2017-08-17 22:00:37 +05:30
}
available() {
2018-11-08 19:23:39 +05:30
this.availableButton.classList.remove('hidden');
this.unavailableButton.classList.add('hidden');
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
bindEvents() {
2018-11-08 19:23:39 +05:30
this.createMergeRequestButton.addEventListener(
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
this.createTargetButton.addEventListener(
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
2021-12-11 22:18:48 +05:30
this.branchInput.addEventListener('input', this.onChangeInput.bind(this));
2018-03-17 18:26:18 +05:30
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
2021-12-11 22:18:48 +05:30
// Detect for example when user pastes ref using the mouse
this.refInput.addEventListener('input', this.onChangeInput.bind(this));
// Detect for example when user presses right arrow to apply the suggested ref
2018-03-17 18:26:18 +05:30
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
2021-12-11 22:18:48 +05:30
// Detect when user clicks inside the input to apply the suggested ref
this.refInput.addEventListener('click', this.onChangeInput.bind(this));
// Detect when user clicks outside the input to apply the suggested ref
this.refInput.addEventListener('blur', this.onChangeInput.bind(this));
// Detect when user presses tab to apply the suggested ref
2018-03-17 18:26:18 +05:30
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
checkAbilityToCreateBranch() {
this.setUnavailableButtonState();
2018-11-08 19:23:39 +05:30
axios
.get(this.canCreatePath)
2018-03-17 18:26:18 +05:30
.then(({ data }) => {
this.setUnavailableButtonState(false);
if (data.can_create_branch) {
this.available();
this.enable();
2018-10-15 14:42:47 +05:30
this.updateBranchName(data.suggested_branch_name);
2018-03-17 18:26:18 +05:30
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
2018-10-15 14:42:47 +05:30
} else {
2018-03-17 18:26:18 +05:30
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
2021-04-29 21:17:54 +05:30
createFlash({
message: __('Failed to check related branches.'),
});
2018-03-17 18:26:18 +05:30
});
}
createBranch() {
this.isCreatingBranch = true;
2018-11-08 19:23:39 +05:30
return axios
2019-09-30 21:07:59 +05:30
.post(createEndpoint(this.projectPath, this.createBranchPath), {
confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null,
})
2018-03-17 18:26:18 +05:30
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
})
2021-04-29 21:17:54 +05:30
.catch(() =>
createFlash({
message: __('Failed to create a branch for this issue. Please try again.'),
}),
);
2018-03-17 18:26:18 +05:30
}
createMergeRequest() {
2022-04-04 11:22:00 +05:30
return new Promise(() => {
this.isCreatingMergeRequest = true;
return this.createBranch().then(() => {
window.location.href = canCreateConfidentialMergeRequest()
? this.createMrPath.replace(
this.projectPath,
confidentialMergeRequestState.selectedProject.pathWithNamespace,
)
: this.createMrPath;
});
});
2018-03-17 18:26:18 +05:30
}
disable() {
this.disableCreateAction();
}
2021-04-29 21:17:54 +05:30
setLoading(loading) {
this.createMergeRequestLoading.classList.toggle('gl-display-none', !loading);
}
2018-03-17 18:26:18 +05:30
disableCreateAction() {
this.createMergeRequestButton.classList.add('disabled');
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
this.createTargetButton.classList.add('disabled');
this.createTargetButton.setAttribute('disabled', 'disabled');
2017-08-17 22:00:37 +05:30
}
enable() {
2019-09-30 21:07:59 +05:30
if (isConfidentialIssue() && !canCreateConfidentialMergeRequest()) return;
2017-08-17 22:00:37 +05:30
this.createMergeRequestButton.classList.remove('disabled');
this.createMergeRequestButton.removeAttribute('disabled');
2018-03-17 18:26:18 +05:30
this.createTargetButton.classList.remove('disabled');
this.createTargetButton.removeAttribute('disabled');
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
static findByValue(objects, ref, returnFirstMatch = false) {
if (!objects || !objects.length) return false;
if (objects.indexOf(ref) > -1) return ref;
2021-03-08 18:12:59 +05:30
if (returnFirstMatch) return objects.find((item) => new RegExp(`^${ref}`).test(item));
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
return false;
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
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',
},
],
hideOnClick: false,
};
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
static getInputSelectedText(input) {
const start = input.selectionStart;
const end = input.selectionEnd;
return input.value.substr(start, end - start);
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
getRef(ref, target = 'all') {
if (!ref) return false;
2021-12-11 22:18:48 +05:30
this.refCancelToken = axios.CancelToken.source();
2018-11-08 19:23:39 +05:30
return axios
2021-12-11 22:18:48 +05:30
.get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`, {
cancelToken: this.refCancelToken.token,
})
2018-03-17 18:26:18 +05:30
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
let result;
if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
2018-11-08 19:23:39 +05:30
result =
CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
2018-03-17 18:26:18 +05:30
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
this.isGettingRef = false;
return this.updateInputState(target, ref, result);
})
2021-12-11 22:18:48 +05:30
.catch((thrown) => {
if (axios.isCancel(thrown)) {
return false;
}
2018-03-17 18:26:18 +05:30
this.unavailable();
this.disable();
2021-04-29 21:17:54 +05:30
createFlash({
message: __('Failed to get ref.'),
});
2018-03-17 18:26:18 +05:30
this.isGettingRef = false;
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
return false;
});
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
getTargetData(target) {
2017-08-17 22:00:37 +05:30
return {
2018-03-17 18:26:18 +05:30
input: this[`${target}Input`],
message: this[`${target}Message`],
2017-08-17 22:00:37 +05:30
};
}
2018-03-17 18:26:18 +05:30
hide() {
2018-11-08 19:23:39 +05:30
this.wrapperEl.classList.add('hidden');
2018-03-17 18:26:18 +05:30
}
init() {
this.checkAbilityToCreateBranch();
}
initDroplab() {
this.droplab = new DropLab();
this.droplab.init(
this.dropdownToggle,
this.dropdownList,
[InputSetter],
this.getDroplabConfig(),
);
}
inputsAreValid() {
return this.branchIsValid && this.refIsValid;
2017-08-17 22:00:37 +05:30
}
isBusy() {
2018-11-08 19:23:39 +05:30
return (
this.isCreatingMergeRequest ||
2017-08-17 22:00:37 +05:30
this.mergeRequestCreated ||
this.isCreatingBranch ||
2018-03-17 18:26:18 +05:30
this.branchCreated ||
2018-11-08 19:23:39 +05:30
this.isGettingRef
);
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
onChangeInput(event) {
2020-01-01 13:55:28 +05:30
this.disable();
2018-03-17 18:26:18 +05:30
let target;
let value;
2021-12-11 22:18:48 +05:30
// User changed input, cancel to prevent previous request from interfering
if (this.refCancelToken !== null) {
this.refCancelToken.cancel();
}
2018-03-17 18:26:18 +05:30
if (event.target === this.branchInput) {
target = 'branch';
2018-11-08 19:23:39 +05:30
({ value } = this.branchInput);
2018-03-17 18:26:18 +05:30
} else if (event.target === this.refInput) {
target = 'ref';
2021-12-11 22:18:48 +05:30
if (event.target === document.activeElement) {
value =
event.target.value.slice(0, event.target.selectionStart) +
event.target.value.slice(event.target.selectionEnd);
} else {
value = event.target.value;
}
2018-03-17 18:26:18 +05:30
} 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);
2021-12-11 22:18:48 +05:30
this.refDebounce(value, target);
2018-03-17 18:26:18 +05:30
return true;
}
this.showCheckingMessage(target);
this.refDebounce(value, target);
return true;
}
onClickCreateMergeRequestButton(event) {
2017-08-17 22:00:37 +05:30
let xhr = null;
2018-03-17 18:26:18 +05:30
event.preventDefault();
2017-08-17 22:00:37 +05:30
2019-09-30 21:07:59 +05:30
if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) {
2021-03-08 18:12:59 +05:30
this.droplab.hooks.forEach((hook) => hook.list.toggle());
2019-09-30 21:07:59 +05:30
return;
}
2017-08-17 22:00:37 +05:30
if (this.isBusy()) {
return;
}
2018-03-17 18:26:18 +05:30
if (event.target.dataset.action === CREATE_MERGE_REQUEST) {
2017-08-17 22:00:37 +05:30
xhr = this.createMergeRequest();
2018-03-17 18:26:18 +05:30
} else if (event.target.dataset.action === CREATE_BRANCH) {
2017-08-17 22:00:37 +05:30
xhr = this.createBranch();
}
2018-03-17 18:26:18 +05:30
xhr.catch(() => {
2017-08-17 22:00:37 +05:30
this.isCreatingMergeRequest = false;
this.isCreatingBranch = false;
2018-03-17 18:26:18 +05:30
this.enable();
2021-04-29 21:17:54 +05:30
this.setLoading(false);
2018-03-17 18:26:18 +05:30
});
2017-08-17 22:00:37 +05:30
2021-04-29 21:17:54 +05:30
this.setLoading(true);
2017-08-17 22:00:37 +05:30
this.disable();
}
2018-03-17 18:26:18 +05:30
onClickSetFocusOnBranchNameInput() {
this.branchInput.focus();
2017-08-17 22:00:37 +05:30
}
2018-03-17 18:26:18 +05:30
// `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();
2021-12-11 22:18:48 +05:30
const caretPositionEnd = this.refInput.value.length;
this.refInput.setSelectionRange(caretPositionEnd, caretPositionEnd);
2018-03-17 18:26:18 +05:30
}
removeMessage(target) {
const { input, message } = this.getTargetData(target);
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
2018-11-08 19:23:39 +05:30
const messageClasses = ['text-muted', 'text-danger', 'text-success'];
2018-03-17 18:26:18 +05:30
2021-03-08 18:12:59 +05:30
inputClasses.forEach((cssClass) => input.classList.remove(cssClass));
messageClasses.forEach((cssClass) => message.classList.remove(cssClass));
2018-03-17 18:26:18 +05:30
message.style.display = 'none';
}
setUnavailableButtonState(isLoading = true) {
if (isLoading) {
2020-04-08 14:13:33 +05:30
this.unavailableButtonSpinner.classList.remove('hide');
2018-03-17 18:26:18 +05:30
this.unavailableButtonText.textContent = __('Checking branch availability...');
} else {
2020-04-08 14:13:33 +05:30
this.unavailableButtonSpinner.classList.add('hide');
2018-03-17 18:26:18 +05:30
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');
2018-11-08 19:23:39 +05:30
message.classList.add('text-success');
2018-03-17 18:26:18 +05:30
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);
2018-11-08 19:23:39 +05:30
message.classList.add('text-muted');
2018-03-17 18:26:18 +05:30
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
message.style.display = 'inline-block';
}
showNotAvailableMessage(target) {
const { input, message } = this.getTargetData(target);
2018-11-08 19:23:39 +05:30
const text =
target === 'branch' ? __('Branch is already taken') : __('Source is not available');
2018-03-17 18:26:18 +05:30
this.removeMessage(target);
input.classList.add('gl-field-error-outline');
2018-11-08 19:23:39 +05:30
message.classList.add('text-danger');
2018-03-17 18:26:18 +05:30
message.textContent = text;
message.style.display = 'inline-block';
}
unavailable() {
2018-11-08 19:23:39 +05:30
this.availableButton.classList.add('hidden');
this.unavailableButton.classList.remove('hidden');
2018-03-17 18:26:18 +05:30
}
2018-10-15 14:42:47 +05:30
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
2018-03-17 18:26:18 +05:30
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.
// 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');
2018-10-15 14:42:47 +05:30
this.updateCreatePaths(target, ref);
2018-03-17 18:26:18 +05:30
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
2018-10-15 14:42:47 +05:30
this.updateCreatePaths(target, ref);
2018-03-17 18:26:18 +05:30
} 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();
}
2017-08-17 22:00:37 +05:30
}
2018-10-15 14:42:47 +05:30
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
2018-11-08 19:23:39 +05:30
const pathReplacement = `$1${encodeURIComponent(ref)}`;
2018-10-15 14:42:47 +05:30
2018-11-08 19:23:39 +05:30
this.createBranchPath = this.createBranchPath.replace(
this.regexps[target].createBranchPath,
pathReplacement,
);
this.createMrPath = this.createMrPath.replace(
this.regexps[target].createMrPath,
pathReplacement,
);
2022-04-04 11:22:00 +05:30
this.wrapperEl.dataset.createMrPath = this.createMrPath;
2018-10-15 14:42:47 +05:30
}
2017-08-17 22:00:37 +05:30
}