Greasy Fork

Greasy Fork is available in English.

TFS Helper

Adds styles and moves things around so that oft-used functions are easier

当前为 2016-11-08 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         TFS Helper
// @namespace    http://jonas.ninja
// @version      1.9.2
// @description  Adds styles and moves things around so that oft-used functions are easier
// @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'
$('body').addClass(topClass)

waitForKeyElements("div.tab-page[rawtitle=Links]", doEverything, false)
waitForKeyElements(".workitem-info-bar > .info-text-wrapper", addTaskIdCopyUtilities, false)

$(document).on('click', '.ijg-js-copyButton', copy)

function doEverything(linksPane) {
  if ($(linksPane).data('moved')) {
    return
  }
  showLinksPane(linksPane)
  stackAllTabs($(linksPane))
  addBlockedButton(linksPane)
  window.setTimeout(function() {
    addTaskIdCopyUtilities(linksPane)
    changeDialogBorderColor(linksPane)
  }, 250)
  refreshNowAndLater();
}

function showLinksPane(linksPane) {
  $(linksPane).data('moved', true)
    .addClass('linksPanel')
    .prepend($("<h3>").addClass('linksPanelHeader')
      .text($(linksPane).attr('rawtitle')))

  var link = $('a[rawtitle=Links]')
  link.closest('td').parent().closest('td').prev().css({'width': '35%' /*, 'padding-right': '12px'*/})
  link.closest('td').prepend(linksPane)
  link.parent().remove()
}

function changeDialogBorderColor(linksPane) {
  // depending on the type of this work item, color the border differently
  var dialog = $(linksPane).closest('.workitem-dialog')
  var caption = dialog.find('a.caption').text()

  if (caption.indexOf('Product Backlog Item ') !== -1) {
    dialog.css('border-color', '#009CCC') // blue
  } else if (caption.indexOf('Bug ') !== -1) {
    dialog.css('border-color', '#CC293D') // red
  } else if (caption.indexOf('Feature ') !== -1) {
    dialog.css('border-color', '#773B93') // purple
  } else { // Task
    dialog.css('border-color', '#E0C252') // yellow
  }
}

function stackAllTabs($linksPane) {
  if (Math.max(window.innerWidth, document.documentElement.clientWidth) >= 1600) {
    // not stackable
  } else {
    // stackable
    var column2 = $linksPane.closest('table.content').parent('.column')
    var column1 = column2.prev()
    column2.add(column1).css({width: '100%', display: 'block'})
  }
  fillVerticalHeight($linksPane)
}
function fillVerticalHeight($linksPane) {
  var column2 = $linksPane.closest('table.content').parent('.column')
  var column1 = column2.prev()
  var modalRect = $linksPane.closest('.work-item-view')[0].getBoundingClientRect()
  var editorContainer = column1.find('.richeditor-editarea')
  if (Math.max(window.innerWidth, document.documentElement.clientWidth) >= 1600) {
    // not stackable
    var editorRect = editorContainer[0].getBoundingClientRect()
    var newheight = modalRect.bottom - editorRect.bottom + editorRect.height - 10
    var col2height = column2.height() - 58
    editorContainer.css('min-height', Math.max(newheight, col2height))
      .children('iframe').css('height', Math.max(newheight, col2height))
  } else {
    // stackable
    var contentHeight = $linksPane.closest('.work-item-view').children('.witform-layout')[0].getBoundingClientRect().bottom
    var newHeight = editorContainer.height() + modalRect.bottom - contentHeight - 8
    editorContainer/*.css('min-height', Math.max(editorContainer.height(), newHeight))*/
      .children('iframe').css('height', Math.max(editorContainer.height(), newHeight))
  }
}

function addTaskIdCopyUtilities() {
  $('.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 id = $header.find('a.caption').text().match(/\d+/)[0]
    var url = $header.find('a.caption').prop('href')
    var formattedUrl = '*' + $header.find('.info-text').text() + '*\n' + url
    var message = makeMessage($header)
    var $container = $('<div class="ijg-copyButtons">')
    var button = $('<button class="ijg-copyButton">')

    $container.append(button.clone().text('ID')    .addClass('ijg-js-copyButton').data('ijgCopyText', id))
      .append(button.clone().text('Link')          .addClass('ijg-js-copyButton').data('ijgCopyText', formattedUrl))
      .append(button.clone().text('Commit Message').addClass('ijg-js-copyButton').data('ijgCopyText', message))

    $header.find('span.info-text').after($container)
  })
}
function makeMessage(el) {
  var parent = $(el).closest('.info-text-wrapper')
  if (parent.length === 0) {
    console.error("The userscript _TFS Helper_ could not identify this task's name")
  }

  return 't' + parent.children('a.caption').text().match(/\d{5,}/)[0] + ' ' + parent.children('.info-text').text().replace(/^dev: /i, "")
}

function addBlockedButton(linksPane) {
  var blockedInput = linksPane.find('[aria-label=Blocked]');
  blockedInput.closest('.control-cell').css({width: 'calc(100% - 60px)', 'min-width': '50px'})
}


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 refreshNowAndLater() {
  window.dispatchEvent(new Event('resize'));
  window.setTimeout(function() {
    window.dispatchEvent(new Event('resize'));
  }, 500);
}

;(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 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;\
  }\
  .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;\
  }\
  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;\
  }\
  .ui-tabs-panel[rawtitle="Description"] .workitemcontrol > div, \
  .ui-tabs-panel[rawtitle="Description"] .workitemcontrol > div > .richeditor-container, \
  [rawtitle="Steps to Reproduce"] .workitemcontrol > div, \
  [rawtitle="Steps to Reproduce"] .workitemcontrol > div > .richeditor-container {\
    height: auto !important;\
  }\
  .ui-tabs-panel .richeditor-editarea {\
    position: relative;\
  }\
  .ui-tabs-panel .richeditor-editarea iframe {\
    resize: vertical;\
  }\
  .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-dialog .ui-dialog-titlebar-progress-container {\
    margin: 0 !important;\
  }\
  .workitem-dialog .ui-resizable-handle {\
    display: none !important;\
  }\
  span.ui-button-icon-primary.ui-icon.ui-icon-closethick {\
    height: 16px;\
    width: 16px;\
    left: -5px;\
    zoom: 2;\
    top: 2px;\
  }\
  .ijg-copyButtons {\
    display: inline-block;\
    font-size: 14px;\
    margin-top: -2px;\
    margin-bottom: -5px;\
  }\
  button.ijg-copyButton {\
    margin-left: 16px;\
    transition: box-shadow 100ms, background-color 250ms 100ms linear;\
    transform: translateY(-2px);\
  }\
  .ijg-copyButton.isHighlighted {\
    transition-delay: 0s;\
    transition-duration: 0s;\
    background-color: rgba(160, 232, 151, 0.6);\
  }'

  var verticalCompactionStyles = '\
  .work-item-view legend {\
    display: none;\
  }\
  div.tfs-tags {\
    margin-bottom: -4px !important;\
    margin-top: -8px;\
  }\
  .toolbar {\
    border: none;\
    margin-top: -8px;\
  }\
  .toolbar .menu-bar > .menu-item, .workitemcontrol .menu-bar > .menu-item {\
    padding: 6px 8px;\
  }\
  .toolbar {\
    height: 83px;\
  }\
  .linksPanel .grid {\
    height: 250px !important;\
    margin-top: -12px;\
  }\
  .ui-dialog .ui-dialog-buttonpane button {\
    margin: 0.3em .4em 0.3em 0;\
  }'

  GM_addStyle("." + topClass + " " + modalStyle)
  GM_addStyle("." + topClass + " " + innerModalStyle)
  GM_addStyle("." + topClass + " " + uiDialogContentStyle)
  GM_addStyle(otherStyles)
  GM_addStyle(verticalCompactionStyles)
})()




function waitForKeyElements(
  // CC BY-NC-SA 4.0. Author: BrockA
  selectorTxt, // Required: The jQuery selector string that specifies the desired element(s).
  actionFunction, // Required: The code to run when elements are found. It is passed a jNode to the matched element.
  bWaitOnce, // Optional: If false, will continue to scan for new elements even after the first match is found.
  iframeSelector // Optional: If set, identifies the iframe to search.
) {
  var targetNodes, btargetsFound;

  if (typeof iframeSelector == "undefined")
    targetNodes = $(selectorTxt);
  else
    targetNodes = $(iframeSelector).contents()
    .find(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,
            iframeSelector
          );
        },
        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);
  }
}