使用 TypeScript 的 TFS JS 扩展
使用 TypeScript 的 TFS JS 扩展。
在开始之前,我想感谢 Tiago Pascal 在 JavaScript 扩展的基础知识、技巧和窍门方面给予我的帮助,以及感谢 Alexander Vanwynsberghe 提供的一个调试技巧,这使得一切都变得更加容易。这篇文章将为您提供创建 TFS Web Access JavaScript 扩展的一个基本起点,其中大部分内容使用正确的 TypeScript 编写。这篇文章将简要概述整个过程,并提供足够的信息供您开始。在以后的文章中,我将更详细地介绍不同的部分,并解释代码片段的作用,或者我如何知道代码片段可以在 Web 访问中使用。
所需文件
首先,您需要为您的扩展取一个名字,我选择了 B1n4ryD1g1t.Tfs.Extensions
,所以当您看到它时,请将其替换为您自己的名字,但我会尽量记住指出需要替换的几个地方。我发现创建这些插件最简单的方法是创建一个新的带有 TypeScript 的 HTML 应用程序,这提供了一个良好的起点,并且对我来说更加熟悉,因为它是一个解决方案,允许我构建项目,并且任何 TypeScript 错误都会通过常规的错误列表窗口向我显示。
创建 2 个文件(并记住在这里替换名称):manifest.xml 和 B1n4ryD1g1t.Tfs.Extensions.min.js,然后将解决方案当前的 app.ts 重命名为 B1n4ryD1g1t.Tfs.Extensions.debug.ts。如果构建解决方案并显示解决方案文件夹中的所有文件,您会注意到现在有一个 B1n4ryD1g1t.Tfs.Extensions.debug.js 和 B1n4ryD1g1t.Tfs.Extensions.debug.js.map 文件,您可以将它们包含在您的解决方案中。设置解决方案的最后一步是删除 app.css、default.htm 和 web.config,因为我们不会用到它们。接下来,您需要使用 NuGet 为您的项目添加对 JQuery 的引用。
现在右键单击您的 jquery-{version}.js 文件,然后单击搜索 TypeScript 类型… 并安装 jquery.TypeScript.DefinitelyTyped 类型。
您的解决方案应该看起来像下面一样,我们不会在扩展中物理引用这些 jquery 文件。它们纯粹用于获取类型定义。
manifest.xml 中包含什么
manifest.xml 包含您的扩展的基本信息,TFS 将在 Web 访问的扩展页面上显示这些信息。
插件节点不言自明,目前,您将保持模块节点不变(记住替换名称)。下面的两个模块允许我们的扩展在任务板和组合板上运行。
<WebAccess version="12.0">
<plug in name="B1n4ryD1g1t Tfs Extensions - Web Access" vendor="Gordon Beeming"
moreinfo="http://gbeeming.wordpress.com" version="1.7">
<modules>
<module namespace="B1n4ryD1g1t.Tfs.Extensions" loadAfter="TFS.Agile.TaskBoard.View"/>
<module namespace="B1n4ryD1g1t.Tfs.Extensions" loadAfter="TFS.Agile.Boards.Controls"/>
</modules>
</plug in>
</WebAccess>
B1n4ryD1g1t.Tfs.Extensions.debug.ts 所需的最少内容是什么?
我们扩展的最小内容将是纯 JavaScript,并在我们的解决方案中留下 4 个可以忽略的错误。
扩展的内容将如下所示
/// <reference path="Scripts/typings/jquery/jquery.d.ts" />
var __extends = this.__extends || function (d, b) { function __() { this.constructor = d; }
__.prototype = b.prototype; d.prototype = new __(); }; define(["require", "exports",
"Presentation/Scripts/TFS/TFS", "Presentation/Scripts/TFS/TFS.Core",
"Presentation/Scripts/TFS/TFS.OM", "Presentation/Scripts/TFS/TFS.UI.Controls",
"WorkItemTracking/Scripts/TFS.WorkItemTracking"], function (require1, exports, tfs, core, tfsOM,
tfsUiControls, tfsWorkItemTracking) { var TFS = tfs; var Core = core; var TFS_OM = tfsOM;
var TFS_UI_Controls = tfsUiControls; var TFS_WorkItemTracking = tfsWorkItemTracking;
var B1n4ryD1g1tTfsExtension = (function (_super) { __extends(B1n4ryD1g1tTfsExtension, _super);
function B1n4ryD1g1tTfsExtension(options) { _super.call(this, options); }
B1n4ryD1g1tTfsExtension.prototype.initializeOptions = function (options)
{ _super.prototype.initializeOptions.call(this, $.extend({ }, options)); };
B1n4ryD1g1tTfsExtension.prototype.initialize = function () { //alert('this is running'); };
B1n4ryD1g1tTfsExtension._typeName = "B1n4ryD1g1t.Tfs.Extensions";
return B1n4ryD1g1tTfsExtension; })
(TFS_UI_Controls.BaseControl); TFS.initClassPrototype(B1n4ryD1g1tTfsExtension, {});
TFS_UI_Controls.Enhancement.registerEnhancement(B1n4ryD1g1tTfsExtension, ".taskboard");
TFS_UI_Controls.Enhancement.registerEnhancement(B1n4ryD1g1tTfsExtension, ".agile-board"); });
Add the extension to TFS Web Access
打开您的 debug.js 文件,将其所有内容复制到 min.js 文件中并进行最小化处理。Visual Studio 的 Web Essentials 扩展将对此有所帮助,因为它与 Ctrl + Alt + X 一样简单。打开您的扩展所在的文件夹,然后将 debug.js、min.js 和 manifest.xml 文件压缩。
现在,您需要将扩展上传到 TFS。浏览到 TFS 服务器主页,点击管理设置按钮,然后点击扩展选项卡。点击安装,浏览您新创建的 zip 文件,然后点击确定。现在您需要做的就是点击启用并点击确定以确认警告。现在只需刷新任何任务板或组合板,您就会看到扩展内部发出的警报。
对扩展进行更改
正如您所见,添加扩展是一个非常漫长的过程,除非您使用 Alexander 在博客中介绍的一个技巧 调试 TFS Web Access 扩展,否则检查更改不会有任何不同。这基本上是使用 fiddler 告诉您的计算机,当它请求您的扩展的 js 文件时,它必须使用您解决方案中的版本 js 文件而不是 TFS 中的文件,这将大大加快开发速度,因为您可以简单地保存文件,这会触发 Visual Studio 更新 debug.js 文件,刷新 TFS 时现在将加载该文件。对于编辑器,我使用了 regex:http://labtfs01/_static/tfs/12/_scripts/TFS/.+//_plugins/.+/B1n4ryD1g1t.Tfs.Extensions.js 的值,其中 header:CachControl=no-cache
和 regex:http://labtfs01/_static/tfs/12/_scripts/TFS/.+//_plugins/.+/B1n4ryD1g1t.Tfs.Extensions.js 以及 C:\c\r\Apps\TfsJsExtensions\TfsJsExtensions\B1n4ryD1g1t.Tfs.Extensions.debug.js。与博客文章类似,只是在 TFS 中为 debug/min 差异添加了通配符。
添加一些真正的 TypeScript
在您的 .ts 文件的底部,添加下面的 TypeScript 代码(抱歉没有完整的 TS 高亮显示)。
module B1n4ryD1g1tModule {
export class Core {
Require: any;
Exports: any;
TFS: any;
Core: any;
TFS_OM: any;
TFS_WorkItemTracking: any;
WorkItemManager: any;
CurrentlyFetchingWorkItems: boolean;
constructor(require1, exports, tfs, core, tfsom, tfsWorkItemTracking) {
this.Require = require1;
this.Exports = exports;
this.TFS = tfs;
this.Core = core;
this.TFS_OM = tfsom;
this.TFS_WorkItemTracking = tfsWorkItemTracking;
}
public init(): any {
this.initWorkItemManagerEvents();
var that = this;
window.setTimeout(function () {
if (that.isAgileBoard()) {
that.setAgileBoardIDs();
}
if (that.isTaskBoard()) {
that.setTaskBoardIDs();
}
}, 100);
}
private getCurrentTeamName(): string {
return this.TFS.Host.TfsContext.getDefault().currentTeam.name;
}
private isTaskBoard(): boolean {
return $(".taskboard").length > 0;
}
private isAgileBoard(): boolean {
return $(".agile-board").length > 0;
}
private setAgileBoardIDs(): void {
var idsToFetch = [];
$(".board-tile").each(function () {
var id = $(this).attr("data-item-id");
idsToFetch.push(parseInt(id));
});
this.loadWorkItems(idsToFetch, this.setAgileBoardIDsWork);
}
private setAgileBoardIDsWork(index, row, that: Core): void {
that.workWithAgileBoard(row[0], that);
}
private workWithAgileBoard(id, that: Core): void {
if (that.workWithAgileBoard_WorkItem(id)) {
}
}
private setTaskBoardIDs(): void {
var idsToFetch = [];
$("#taskboard-table .tbTile").each(function () {
var id = $(this).attr("id");
id = id.split('-')[1];
idsToFetch.push(parseInt(id));
});
$("#taskboard-table .taskboard-row .taskboard-parent").each(function () {
var id = $(this).attr("id");
if (id != undefined) {
id = id.split('_')[1];
id = id.substring(1);
idsToFetch.push(parseInt(id));
}
});
this.loadWorkItems(idsToFetch, this.setTaskBoardIDsWork);
}
private setTaskBoardIDsWork(index, row, that: Core): void {
if (that.workWithTaskBoard(row[0], row[1], that)) {
}
}
private workWithTaskBoard(id, state, that: Core): void {
if (that.workWithTaskBoard_Task(id)) {
}
if (that.workWithTaskBoard_Requirement(id)) {
that.taskboard_setRequirementState(id, state);
}
}
private workWithTaskBoard_Requirement(id): boolean {
return this.boards_setID("taskboard-table_p" + id, id);
}
private workWithTaskBoard_Task(id): boolean {
return this.boards_setID("tile-" + id, id);
}
private workWithAgileBoard_WorkItem(id): boolean {
var titleObj = $(".board-tile[data-item-id='" + id + "'] .title");
if ($(titleObj).length > 0 && $(titleObj).find(".TitleAdded").length == 0) {
var idHtml = "<span class='TitleAdded'
style='font-weight:bold;'>" + id + "</span> - ";
$(titleObj).html(idHtml + $(titleObj).html());
return true;
}
return false;
}
private boards_setID(idTagLookFor, id): boolean {
var titleObj = $("#" + idTagLookFor + " .witTitle");
if ($(titleObj).length > 0 && $(titleObj).find(".TitleAdded").length == 0) {
var idHtml = "<span class='TitleAdded'
style='font-weight:bold;'>" + id + "</span> - ";
$(titleObj).html(idHtml + $(titleObj).html());
return true;
}
return false;
}
private taskboard_setRequirementState(id, state): boolean {
var titleObj = $("#taskboard-table_p" + id + " .witTitle");
if ($(titleObj).length > 0 && $(titleObj).find(".StateAdded").length == 0) {
var stateHtml = "<br/><br/><span class='StateAdded'
style='color:#505050;font-size:smaller;font-weight:bold;'>" + state + "</span>";
$(titleObj).html($(titleObj).html() + stateHtml);
return true;
}
return false;
}
private workItemChanged(sender, workItemChangedArgs): void {
if (workItemChangedArgs.change === this.TFS_WorkItemTracking.WorkItemChangeType.Reset ||
workItemChangedArgs.change === this.TFS_WorkItemTracking.WorkItemChangeType.SaveCompleted) {
var that = this;
var id = workItemChangedArgs.workItem.id;
var state = workItemChangedArgs.workItem.getFieldValue("System.State");
if (that.isTaskBoard()) {
window.setTimeout(function () {
that.workWithTaskBoard(id, state, that);
}, 100);
} else if (that.isAgileBoard()) {
window.setTimeout(function () {
that.workWithAgileBoard(id, that);
}, 100);
}
}
}
private loadWorkItems(idsToFetch: Array, onComplete): void {
var that = this;
that.loadWorkItemsWork(idsToFetch, onComplete, that);
}
private loadWorkItemsWork(idsToFetch: Array, onComplete, that: Core): void {
var takeAmount = 100;
if (takeAmount >= idsToFetch.length) {
takeAmount = idsToFetch.length;
}
if (takeAmount > 0) {
that.WorkItemManager.store.beginPageWorkItems(idsToFetch.splice(0, takeAmount), [
"System.Id",
"System.State"
], function (payload) {
that.loadWorkItemsWork(idsToFetch, onComplete, that);
$.each(payload.rows, function (index, row) {
onComplete(index, row, that);
});
}, function (err) {
that.loadWorkItemsWork(idsToFetch, onComplete, that);
alert(err);
});
}
}
private initWorkItemManagerEvents(): void {
var service = this.TFS_OM.TfsTeamProjectCollection.getDefaultConnection().getService
(this.TFS_WorkItemTracking.WorkItemStore);
this.WorkItemManager = service.workItemManager;
var that = this;
this.WorkItemManager.attachWorkItemChanged(function (sender, workItemChangedArgs) {
that.workItemChanged(sender, workItemChangedArgs);
});
}
}
}
这就是完成神奇效果所需的所有代码,剩下的就是将其连接到扩展。这可以通过替换扩展原始片段中的以下方法来完成。
B1n4ryD1g1tTfsExtension.prototype.initialize = function () {
var bdCore = new B1n4ryD1g1tModule.Core
(require1, exports, TFS, Core, TFS_OM, TFS_WorkItemTracking);
bdCore.init();
};
如果现在刷新页面,并且调试技巧正在运行,您将看到您的工作项有 ID,并且在任务板上,需求状态正在显示。
以前
操作后
希望这能像它在我身上一样激发他人的灵感。正如引言中所述,我将在以后的文章中扩展本文涵盖的内容,以提供更多细节。