NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Jira Utils
// @namespace https://github.com/TheBit/user-script-copy-jira-info-for-git
// @version 2.0.2
// @description Helpful jira functionality
// @author TheBit, D4ST1N
// @license MIT
// @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js
// @require https://unpkg.com/axios/dist/axios.min.js
// @match https://jira.betlab.com/browse/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// jira integration parameters
const jip = {
ticketNameSelector: '#summary-val',
ticketIDSelector: '#key-val',
ticketTypeSelector: '#type-val',
containerSelector: '.ops-menus.aui-toolbar',
ticketTypes: {
task: 'task',
story: 'story',
techTask: 'tech task',
defect: 'defect',
productionDefect: 'production defect',
},
getTicketName() {
return document.querySelector(this.ticketNameSelector).innerText;
},
getTicketID() {
return document.querySelector(this.ticketIDSelector).innerText;
},
getTicketType() {
return document.querySelector(this.ticketTypeSelector).innerText;
},
getContainer() {
return document.querySelector(this.containerSelector);
}
};
// gitLab integration parameters
const gip = {
access: {
param: 'private_token',
value: '5VK28u4H9d39NFv1r7sv',
},
filter: {
param: 'search',
},
pageSize: {
param: 'per_page',
value: 50,
},
brandsBranchesFilter: 'master',
hotfixBranchesFilter: 'release',
projects: {
mobile: {
key: 'air/air-mobile',
name: 'air-mobile',
type: 'mobile',
mainBranch: 'master',
mainBrand: 'com',
},
desktop: {
key: 'air/air-pm',
name: 'air-pm',
type: 'desktop',
mainBranch: 'develop',
mainBrand: 'com',
},
},
url: 'https://git.betlab.com/api/v4/projects/{project}/repository/branches?{params}',
branchUrl: 'https://git.betlab.com/{project}/tree/{branch}',
getGitLabUrl(project, params) {
return this.url.replace('{project}', encodeURIComponent(project)).replace('{params}', params);
},
getGitLabBranchUrl(project, branch) {
return this.branchUrl.replace('{project}', project).replace('{branch}', branch);
}
};
const notification = {
config: {
delay: 1500,
},
setStyles() {
const style = document.createElement('style');
style.innerHTML = `
.ju-notice {
position: absolute;
z-index: 100;
background: #455A64;
color: #fff;
display: flex;
padding: 5px 10px;
border-radius: 5px;
transform: translate(-50%, -100%);
animation-name: notice-out;
animation-delay: .25s;
animation-duration: 1.25s;
animation-fill-mode: forwards;
}
@keyframes notice-out {
0% {
opacity: 1;
transform: translate(-50%, -100%);
}
100% {
opacity: 0;
transform: translate(-50%, -200%);
}
}`;
document.body.appendChild(style);
},
getNotificationID() {
return Date.now();
},
createNotification() {
const notification = document.createElement('div');
notification.className = 'ju-notice';
return notification;
},
getCoordinates(element) {
const box = element.getBoundingClientRect();
return {
top: box.top + pageYOffset,
left: box.left + pageXOffset
};
},
show({element, message, config = this.config}) {
if (element.$juNoticeID) {
return;
}
const notification = this.createNotification();
const elementPosition = this.getCoordinates(element);
notification.innerHTML = message;
notification.style.top = `${elementPosition.top}px`;
notification.style.left = `${elementPosition.left + element.offsetWidth / 2}px`;
document.body.appendChild(notification);
const id = this.getNotificationID();
const timer = setTimeout(() => {
document.body.removeChild(notification);
delete element.$juNoticeID;
clearTimeout(timer);
}, config.delay);
element.$juNoticeID = id;
}
};
Vue.component(
'juactions',
{
template: `
<div class="juactions-root">
<div class="buttons-group">
Branch:
<div
v-for="issueType in issueTypes"
:key="issueType.key"
:class="{'action-button': true, 'action-button--selected': issueType.selected}"
@click="selectType(issueTypes, issueType)"
>
{{ issueType.label }}
</div>
</div>
<div class="group-separator"></div>
<div class="buttons-group">
From:
<div
v-for="platform in platforms"
:key="platform.key"
:class="{'action-button': true, 'action-button--selected': platform.selected}"
@click="selectType(platforms, platform)"
>
{{ platform.label }}
</div>
</div>
<div class="buttons-group">
<div
v-for="branch in branches"
:key="branch.key"
:class="{'action-button': true, 'action-button--selected': branch.selected}"
@click="selectType(branches, branch, false)"
>
{{ branch.label }}
</div>
</div>
<div class="group-separator" v-if="showBranchName"></div>
<div class="branch-name-block" v-show="showBranchName">
To:
<input type="text" disabled class="ajs-dirty-warning-exempt branch-name__constant-part" :value="constantPart">
<input type="text" class="ajs-dirty-warning-exempt branch-name__editable-part" v-model="editablePart" ref="editablePart" @input="formatBranchName">
<div class="group-separator"></div>
<div class="action-button copyButton" :data-clipboard-text="newBranchName" @click="copy">
<span class="action-button__title">Copy</span>
<span class="aui-icon aui-icon-small aui-iconfont-copy-clipboard action-button__icon"></span>
</div>
<div :class="{'action-button': true, 'action-button--view': branchExists, 'action-button--fork': !branchExists }" @click="createBranch">
<span class="action-button__title">{{ branchExists ? 'View' : 'Fork' }}</span>
<span
:class="{
'aui-icon': true,
'aui-icon-small': true,
'aui-iconfont-devtools-branch': !branchExists,
'aui-iconfont-view': branchExists,
'action-button__icon': true
}">
</span>
</div>
</div>
<div class="group-separator"></div>
<div class="buttons-group">
<div class="action-button copyButton" :data-clipboard-text="commitMessage" @click="copy">
<span class="action-button__title">Commit message</span>
<span class="aui-icon aui-icon-small aui-iconfont-devtools-commit action-button__icon"></span>
</div>
</div>
</div>`,
data() {
return {
styles: document.createElement('style'),
baseStyles: `
#juapp {
display: flex;
width: 100%;
}
.juactions-root {
display: flex;
width: 100%;
padding: 5px 10px;
background: #CFD8DC;
align-items: center;
}
.buttons-group {
margin-right: 10px;
max-width: 30%;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.action-button {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 14px;
background-color: #f5f5f5;
color: #333;
padding: 3px 10px;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
transition: all .375s;
}
.action-button--selected {
background-color: #4CAF50;
color: #fff;
}
.action-button--view {
background-color: #90A4AE;
color: #fff;
}
.action-button--fork {
background-color: #FFB74D;
color: #fff;
}
.action-button:hover {
margin-left: 0;
}
.action-button__icon {
pointer-events: none;
}
.action-button--view .action-button__icon,
.action-button--fork .action-button__icon {
color: #fff;
}
.action-button__icon:not(:first-child) {
margin-left: 5px;
}
.action-button__title {
pointer-events: none;
}
.branch-name-block {
display: flex;
align-items: center;
}
.branch-name__constant-part {
height: 28px;
box-sizing: border-box;
padding: 3px 0 3px 8px;
background-color: #fff;
border: 1px solid #ccc;
color: #666;
opacity: .8;
margin-left: 5px;
width: 150px;
text-align: right;
}
.branch-name__editable-part {
height: 28px;
box-sizing: border-box;
padding: 3px 8px 3px 0;
}
.group-separator {
height: 80%;
width: 1px;
background-color: #d3d3d3;
border-right: 1px solid #aaa;
margin-left: 5px;
margin-right: 15px;
}`,
issueTypes: [
{
label: 'Feature',
key: 'feature',
selected: false,
filter: gip.brandsBranchesFilter,
},
{
label: 'Bugfix',
key: 'bugfix',
selected: false,
filter: gip.brandsBranchesFilter,
},
{
label: 'Hotfix',
key: 'hotfix',
selected: false,
filter: gip.hotfixBranchesFilter,
},
],
platforms: [
{
label: gip.projects.desktop.name,
key: gip.projects.desktop.key,
selected: false,
},
{
label: gip.projects.mobile.name,
key: gip.projects.mobile.key,
selected: false,
},
],
branches: [],
selectedBranchKey: '',
initialBranchLabel: '',
desktop: gip.projects.desktop.key,
mobile: gip.projects.mobile.key,
showBranchName: false,
constantPart: '',
editablePart: '',
clipboard: undefined,
branchExists: false,
existedBranch: undefined,
commitMessage: '',
};
},
mounted() {
notification.setStyles();
this.createStyles();
this.setCommitMessage();
this.setInitialValues();
this.clipboard = new Clipboard('.copyButton');
},
watch: {
showBranchName() {
this.editablePart = this.getEditablePart();
this.$nextTick(() => {
this.fixEditablePartWidth();
});
},
},
computed: {
newBranchName() {
return `${this.constantPart}${this.editablePart}`;
}
},
methods: {
createStyles() {
document.body.appendChild(this.styles);
this.styles.innerHTML = this.baseStyles;
},
setInitialValues() {
const initialPlatform = this.getInitialPlatform();
const initialIssueType = this.getInitialIssueType();
this.initialBranchLabel = this.getInitialBrand();
this.issueTypes.filter(issueType => issueType.key === initialIssueType)[0].selected = true;
this.selectType(this.platforms, this.platforms.filter(platform => platform.label === initialPlatform)[0]);
},
fixEditablePartWidth() {
const temp = document.createElement('div');
temp.innerHTML = this.$refs.editablePart.value;
document.body.appendChild(temp);
temp.style.display = 'inline-block';
document.body.appendChild(temp);
this.$refs.editablePart.style.width = `${temp.offsetWidth + 10}px`;
document.body.removeChild(temp);
},
formatBranchName() {
this.editablePart = this.formatToValidBranchName(this.editablePart.replace(/\s/ig, '-'), false);
this.$nextTick(() => {
this.fixEditablePartWidth();
});
},
getSelected(array) {
return array.filter(item => item.selected)[0];
},
selectType(types, selectedType, getBranches = true) {
let previousSelected;
types.forEach((type) => {
if (type.selected) {
previousSelected = type;
}
type.selected = false;
});
selectedType.selected = true;
this.setConstantPart();
if (getBranches && previousSelected !== selectedType) {
const selectedPlatform = this.getSelected(this.platforms);
const selectedIssueType = this.getSelected(this.issueTypes);
if (selectedPlatform && selectedIssueType) {
this.getBranches(selectedPlatform.key, selectedIssueType.filter);
}
}
},
setCommitMessage() {
const ticket = jip.getTicketID();
const ticketName = jip.getTicketName();
const ticketNameFormatted = ticketName.replace(/\s\s/ig, ' ').replace(/\[.*?\]/ig, '').trim();
this.commitMessage = `[${ticket}] ${ticketNameFormatted}`;
},
setConstantPart() {
const ticket = jip.getTicketID();
const issueType = this.issueTypes.filter(issue => issue.selected)[0];
const brand = this.branches.filter(branch => branch.selected)[0];
if (!brand || !issueType) {
this.showBranchName = false;
return;
}
this.showBranchName = true;
this.constantPart = `${issueType.key}/${brand.label}/${ticket}-`;
this.checkBranchExists();
},
getTicketName() {
return document.querySelector(jip.ticketNameSelector).innerText;
},
getTicketID() {
return document.querySelector(jip.ticketIDSelector).innerText;
},
getEditablePart() {
if (this.editablePart) {
return this.editablePart;
}
const ticketName = jip.getTicketName();
return this.formatToValidBranchName(ticketName);
},
formatToValidBranchName(string, limit = 5) {
let formattedString = string.replace(/["'`]/g, '') /* remove quotes */
.replace(/\s-\s/g, '') /* remove dashes */
.replace(/\s\s/ig, ' ') /* Replace double spaces with single space */
.replace(/\[.*?]/ig, '') /* Strip tags in square brackets e.g.: [tag] */
.trim() /* After removing square brackets - there might be leading white space left, so need to trim */
.replace(/\s/ig, '-') /* Replace all spaces with dashes */
.replace(/^[./]|\.\.|@{|[\/.]$|^@$|[~^:\x00-\x20\x7F\s?*[\]\\]/ig, ''); /* Strip all forbidden chars */
return limit ? formattedString.split('-').slice(0, limit).join('-') : formattedString;
},
getBranches(type, search) {
const params = [
{
key: gip.access.param,
value: gip.access.value,
},
{
key: gip.filter.param,
value: search,
},
{
key: gip.pageSize.param,
value: gip.pageSize.value,
},
];
axios.get(gip.getGitLabUrl(type, this.processParams(params)))
.then((response) => {
console.log(response);
this.filterBranches(response.data, type, search);
this.setConstantPart();
})
.catch((error) => {
console.log(error);
});
},
checkBranchExists() {
const selectedPlatform = this.getSelected(this.platforms);
const params = [
{
key: gip.access.param,
value: gip.access.value,
},
{
key: gip.filter.param,
value: this.constantPart,
},
];
axios.get(gip.getGitLabUrl(selectedPlatform.key, this.processParams(params)))
.then((response) => {
console.log(response);
this.branchExists = response.data.length > 0;
this.existedBranch = response.data[0];
})
.catch((error) => {
console.log(error);
});
},
filterBranches(branches, type, search) {
const selectedBranch = this.getSelected(this.branches);
if (selectedBranch) {
this.selectedBranchKey = selectedBranch.key;
}
if (type === this.desktop && search !== gip.hotfixBranchesFilter) {
this.branches = [
{
label: gip.projects.desktop.mainBrand,
key: gip.projects.desktop.mainBranch,
value: gip.projects.desktop.mainBranch,
selected: this.initialBranchLabel === gip.projects.desktop.mainBrand
|| this.selectedBranchKey === gip.projects.desktop.mainBranch
|| false,
},
];
} else if (type === this.mobile && search !== gip.hotfixBranchesFilter) {
this.branches = [
{
label: gip.projects.mobile.mainBrand,
key: gip.projects.mobile.mainBranch,
value: gip.projects.mobile.mainBranch,
selected: this.initialBranchLabel === gip.projects.mobile.mainBrand
|| this.selectedBranchKey === gip.projects.mobile.mainBranch
|| false,
},
];
} else {
this.branches = [];
}
branches.forEach((branch) => {
if (search === gip.brandsBranchesFilter) {
const branchNameParts = branch.name.split('-');
if (branchNameParts.length > 1) {
this.branches.push(
{
key: branch.name,
label: branchNameParts[1],
value: branchNameParts[1],
selected: this.initialBranchLabel === branchNameParts[1] || this.selectedBranchKey === branch.name || false,
}
);
}
} else if (search === gip.hotfixBranchesFilter) {
const branchNameParts = branch.name.split('/');
if (branchNameParts.length > 1 && branch.name.indexOf(new Date().getFullYear()) !== -1) {
let branchBrand = this.getBranchBrand(branchNameParts[0]);
if (branchBrand === gip.hotfixBranchesFilter
|| branchBrand === gip.projects.mobile.type
|| branchBrand === gip.projects.desktop.type
) {
branchBrand = gip.projects.desktop.mainBrand;
}
const branchIndex = this.getBranchIndexByBrand(branchBrand);
const branchObject = {
key: branch.name,
label: branchBrand,
value: branchNameParts[1],
selected: this.initialBranchLabel === branchBrand || this.selectedBranchKey === branch.name || false,
};
if (branchIndex !== undefined) {
const existDate = new Date(this.branches[branchIndex].value);
const currentDate = new Date(branchNameParts[1]);
if (currentDate > existDate) {
this.branches[branchIndex] = branchObject;
}
} else {
this.branches.push(branchObject);
}
}
}
});
this.initialBranchLabel = undefined;
},
getBranchBrand(branch) {
const branchParts = branch.split('-');
return branchParts[branchParts.length - 1];
},
getBranchIndexByBrand(brand) {
let branchIndex;
this.branches.forEach((branch, index) => {
if (branch.label === brand) {
branchIndex = index;
}
});
return branchIndex;
},
processParams(params) {
return params.map(param => `${param.key}=${param.value}`).join('&');
},
createBranch(e) {
const selectedPlatform = this.getSelected(this.platforms);
if (this.branchExists) {
const win = window.open(gip.getGitLabBranchUrl(selectedPlatform.key, this.existedBranch.name), '_blank');
win.focus();
} else {
const selectedBranch = this.getSelected(this.branches);
const params = [
{
key: gip.access.param,
value: gip.access.value,
},
{
key: 'branch',
value: this.newBranchName,
},
{
key: 'ref',
value: selectedBranch.key,
},
];
axios.post(gip.getGitLabUrl(selectedPlatform.key, this.processParams(params)))
.then((response) => {
console.log(response);
notification.show({ element: e.target, message: 'Created :)' });
this.checkBranchExists();
})
.catch((error) => {
console.log(error);
});
}
},
copy(e) {
notification.show({ element: e.target, message: 'Copied :)' });
},
getInitialIssueType() {
const issueType = jip.getTicketType().toLowerCase().trim();
const issueTypesMapping = {
feature: [
jip.ticketTypes.task,
jip.ticketTypes.story,
jip.ticketTypes.techTask,
],
bugfix: [
jip.ticketTypes.defect
],
hotfix: [
jip.ticketTypes.productionDefect
],
};
let initialIssueType;
Object.entries(issueTypesMapping).forEach(([key, value]) => {
if (value.includes(issueType)) {
initialIssueType = key;
}
});
return initialIssueType;
},
getInitialPlatform() {
const ticketName = jip.getTicketName();
if (ticketName.toLowerCase().indexOf(gip.projects.mobile.type) !== -1) {
return gip.projects.mobile.name;
}
return gip.projects.desktop.name;
},
getInitialBrand() {
// TODO remove hardcoded brands
const ticketName = jip.getTicketName();
if (ticketName.toLowerCase().indexOf('[cy]') !== -1) {
return 'cy';
} else if (ticketName.toLowerCase().indexOf('[ge]') !== -1) {
return 'ge';
} else if (ticketName.toLowerCase().indexOf('[ru]') !== -1) {
return 'ru';
} else if (ticketName.toLowerCase().indexOf('[mt]') !== -1) {
return 'mt';
} else if (ticketName.toLowerCase().indexOf('[tz]') !== -1) {
return 'tz';
}
return 'com';
}
},
}
);
const appRoot = document.createElement('div');
appRoot.id = 'juapp';
appRoot.innerHTML = '<juactions></juactions>';
const appContainer = jip.getContainer();
appContainer.appendChild(appRoot);
new Vue(
{
el: '#juapp',
}
);
})();