// ==UserScript==
// @name TFS 2017 Helper
// @namespace http://jonas.ninja
// @version 1.10.0
// @description Adds handy functionality to TFS 2017
// @author @_jnblog
// @match http://*/tfs/DefaultCollection/*/_backlogs*
// @match http://*/tfs/DefaultCollection/*/_versionControl*
// @match http://*/tfs/DefaultCollection/*/_workitems*
// @grant GM_addStyle
// @grant GM_setClipboard
// ==/UserScript==
/* jshint -W097 */
/* global GM_addStyle */
/* jshint asi: true, multistr: true */
var $ = unsafeWindow.jQuery;
var topClass = "makeTfsNotAwful"
var cursorUrl = 'url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDQxNS41ODIgNDE1LjU4MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDE1LjU4MiA0MTUuNTgyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPHBhdGggZD0iTTQxMS40Nyw5Ni40MjZsLTQ2LjMxOS00Ni4zMmMtNS40ODItNS40ODItMTQuMzcxLTUuNDgyLTE5Ljg1MywwTDE1Mi4zNDgsMjQzLjA1OGwtODIuMDY2LTgyLjA2NCAgIGMtNS40OC01LjQ4Mi0xNC4zNy01LjQ4Mi0xOS44NTEsMGwtNDYuMzE5LDQ2LjMyYy01LjQ4Miw1LjQ4MS01LjQ4MiwxNC4zNywwLDE5Ljg1MmwxMzguMzExLDEzOC4zMSAgIGMyLjc0MSwyLjc0Miw2LjMzNCw0LjExMiw5LjkyNiw0LjExMmMzLjU5MywwLDcuMTg2LTEuMzcsOS45MjYtNC4xMTJMNDExLjQ3LDExNi4yNzdjMi42MzMtMi42MzIsNC4xMTEtNi4yMDMsNC4xMTEtOS45MjUgICBDNDE1LjU4MiwxMDIuNjI4LDQxNC4xMDMsOTkuMDU5LDQxMS40Nyw5Ni40MjZ6IiBmaWxsPSIjMmQ5ZTFlIi8+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==), auto !important'
var colorMap = {'rgb(0, 156, 204)' : 'pbi',
'rgb(204, 41, 61)' : 'bug',
'rgb(242, 203, 29)' : 'task',
'rgb(119, 59, 147)' : 'feature'}
var templates = {
button: $('<button class="ijg-copyButton">')
}
$('body').addClass(topClass)
waitForKeyElements(".workitem-dialog", changeDialogBorderColor, false)
waitForKeyElements(".workitem-info-bar > .info-text-wrapper", addCopyUtilities, false)
$(document).on('click', '.ijg-js-copyButton', copy)
function changeDialogBorderColor (workitemDialog) {
// depending on the type of this work item, color the border differently
var dialog = $(workitemDialog)
var borderColor = dialog.find('.work-item-form-main-header').css('border-left-color')
var itemType = colorMap[borderColor]
// retry if necessary.
if (borderColor === undefined) {
window.setTimeout(function() {
changeDialogBorderColor(workitemDialog)
}, 100)
}
if (itemType === 'pbi') {
dialog.css({'border-color': borderColor,
'box-shadow' : '#91c3d2 0 0 30px 8px'})
} else if (itemType === 'bug') {
dialog.css({'border-color': borderColor,
'box-shadow': '#a15d5d 0 0 30px 8px'})
} else if (itemType === 'feature') {
dialog.css({'border-color': borderColor,
'box-shadow': '#ac80ac 0 0 30px 8px'})
} else if (itemType === 'task') {
dialog.css({'border-color': borderColor,
'box-shadow' : '#ddd3ae 0 0 30px 8px'})
}
}
function addCopyUtilities () {
$('.workitem-info-bar').find('.info-text-wrapper').each(function (idx, header) {
var $header = $(header)
if ($header.hasClass('ijg-isProcessed')) {
return
}
$header.addClass('ijg-isProcessed')
var itemLink = $header.find('a.caption')
var formattedItemLinkText = itemLink.text().replace(/^Product Backlog Item/i, 'PBI')
var id = itemLink.text().match(/\d+/)[0]
var url = itemLink.prop('href')
var text = $header.closest('.ui-dialog-content').find('.work-item-form-title input').val()
var formattedUrl = '*' + formattedItemLinkText + ': ' + text + '*\n' + url
var message = text.replace(/^dev: */i, "")
var $container = $('<div class="ijg-copyButtons">')
$container
.append(makeCopyButton('Link', 'ijg-js-copyButton ijg-copyButton--compact', formattedUrl))
.append(makeCopyButton('ID', 'ijg-js-copyButton ijg-copyButton--compact', id))
.append(makeCopyButton('Commit Message', 'ijg-js-copyButton ijg-copyButton--compact', message))
$header.append($container)
})
}
function makeCopyButton (text, classes, copyData) {
return templates.button.clone()
.text(text).addClass(classes).data('ijgCopyText', copyData)
}
function copy (e) {
$target = $(this)
GM_setClipboard($target.data('ijgCopyText'))
displayResult($target)
}
function displayResult ($button) {
var cursorClass = 'ijg-check'
var highlightClass = 'isHighlighted'
$button.addClass(cursorClass).addClass(highlightClass)
setGreenCheckCursor()
window.setTimeout(function () {
$button.removeClass(highlightClass)
}, 50)
window.setTimeout(function () {
$button.removeClass(cursorClass)
}, 1500)
}
;(function addStyles () {
var modalStyle = '.workitem-dialog { \
left: 10px !important;\
top: 10px !important;\
width: calc(100% - 20px) !important;\
height: calc(100% - 20px) !important;\
border: 4px solid grey;\
box-shadow: gray 0 0 30px 8px;\
box-sizing: border-box;\
}'
var dialogStyles = '\
.ijg-linksPane-createTasksContainer {\
position: absolute;\
top: 10px;\
left: 50%;\
transform: translateX(-50%);\
}'
var innerModalStyle = '.work-item-view {\
width: calc(100% - 20px);\
margin: 0 10px;\
top: 68px;\
}'
var uiDialogContentStyle = '.ui-dialog-content:not(.modal-dialog) {height: calc(100% - 51px) !important}'
var otherStyles = '.' + topClass + ' table.witform-layout {\
width: calc(100% - 4px);\
}\
.linksPanel {\
display: block !important;\
margin-bottom: 5px;\
position: relative;\
}\
.linksPanelHeader {\
background-color: #e6e6e6;\
font-size: 11px;\
text-transform: uppercase;\
margin: 0;\
padding: 0 4px 0;\
border: 0;\
white-space: nowrap;\
height: 25px;\
line-height: 2.1;\
}\
.tbTile {\
width: 100%;\
margin: 0px 0px 3px;\
}\
.subColumn { \
width: calc(50% - 5px); \
margin-right: 5px; \
}\
.linksPanel .grid-cell:not(:only-child) {\
text-indent: 0 !important;\
}\
.linksPanel .grid-row.ijg-is-done {\
background-color: #e9fce8;\
color: #646464;\
}\
button {\
transition: box-shadow 100ms;\
}\
button:focus {\
background-color: #f8f8f8;\
box-shadow: 0px 0px 0px 3px rgba(128, 128, 128, 0.4);\
}\
button:hover {\
background-color: #fefefe;\
}\
button:active {\
background-color: #e6e6e6;\
}\
button.changeset-identifier {\
vertical-align: top;\
line-height: 0;\
padding: 0px 12px;\
height: 22px;\
margin-left: 8px;\
}\
.agile-content-container div.board-tile.ui-draggable,\
#taskboard-table-body .ui-draggable {\
transition: box-shadow 250ms;\
}\
.agile-content-container div.board-tile.ui-draggable:focus,\
#taskboard-table-body .ui-draggable:focus {\
box-shadow: 0px 0px 8px 2px rgb(25, 22, 6);\
transition-delay: 50ms;\
outline: none;\
}\
.witform-layout > tbody > tr.group {\
width: calc(100% - 4px);\
}\
.link-dialog-form .text {\
text-shadow: 0px 0px 6px black;\
height: auto;\
}\
.workitem-control-maximized-dialog {\
height: calc(100% - 20px) !important;\
width: calc(100% - 20px) !important;\
}\
.taskboard-parent {\
min-width: 154px;\
width: 154px;\
}\
.taskboardTableHeaderScrollContainer .taskboard-parent {\
min-width: 164px;\
}\
.ijg-check {\
cursor: ' + cursorUrl + ';\
}\
.workitem-info-bar .info-text-wrapper{\
overflow: visible !important;\
}\
.workitem-header-bar {\
overflow: visible !important;\
z-index: 1;\
}\
.workitem-dialog .ui-dialog-titlebar-progress-container {\
margin: 0 !important;\
}\
.workitem-dialog .ui-resizable-handle {\
display: none !important;\
}\
\
.ui-dialog .full-screen-button,\
.ui-dialog .ui-dialog-titlebar-close{\
transition: background-color 200ms;\
}\
.ui-dialog .full-screen-button:hover {\
background-color: #dcebfc;\
}\
.ui-dialog .ui-dialog-titlebar-close {\
height: 30px !important;\
}\
.ui-dialog .ui-dialog-titlebar-close:hover {\
background-color: rgba(232, 129, 129, 0.5) !important;\
}\
\
.ijg-copyButtons {\
display: inline-block;\
vertical-align: top;\
font-size: 14px;\
margin-top: -2px;\
margin-bottom: -5px;\
}\
button.ijg-copyButton {\
margin-left: 16px;\
transition: box-shadow 100ms, background-color 250ms 100ms linear;\
}\
button.ijg-copyButton.ijg-copyButton--compact {\
height: 25px;\
padding-top: 1px;\
}\
.ijg-copyButton.isHighlighted {\
transition-delay: 0s;\
transition-duration: 0s;\
background-color: rgba(160, 232, 151, 0.6);\
}'
GM_addStyle("." + topClass + " " + modalStyle)
GM_addStyle("." + topClass + " " + innerModalStyle)
GM_addStyle("." + topClass + " " + uiDialogContentStyle)
GM_addStyle(otherStyles + dialogStyles)
})()
function waitForKeyElements (
// CC BY-NC-SA 4.0. Author: BrockA
selectorTxt,
actionFunction,
bWaitOnce
) {
var targetNodes, btargetsFound;
targetNodes = $(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they
are new.
*/
targetNodes.each(function() {
var jThis = $(this);
var alreadyFound = jThis.data('alreadyFound') || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound)
btargetsFound = false;
else
jThis.data('alreadyFound', true);
}
});
} else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey]
} else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function() {
waitForKeyElements(selectorTxt,
actionFunction,
bWaitOnce
);
},
300
);
controlObj[controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
function setGreenCheckCursor() {
/// from https://bugs.chromium.org/p/chromium/issues/detail?id=26723#c87
if (document.body.style.cursor != cursorUrl) {
var wkch = document.createElement("div");
wkch.style.overflow = "hidden";
wkch.style.position = "absolute";
wkch.style.left = "0px";
wkch.style.top = "0px";
wkch.style.width = "100%";
wkch.style.height = "100%";
var wkch2 = document.createElement("div");
wkch2.style.width = "200%";
wkch2.style.height = "200%";
wkch.appendChild(wkch2);
document.body.appendChild(wkch);
document.body.style.cursor = cursorUrl;
wkch.scrollLeft = 1;
wkch.scrollLeft = 0;
document.body.removeChild(wkch);
}
}