Click here to Skip to main content
15,879,474 members
Articles / Programming Languages / Typescript

TFS JS Extension using TypeScript

Rate me:
Please Sign up or sign in to vote.
4.75/5 (3 votes)
29 Sep 2013CPOL5 min read 30.4K   292   6   1
TFS JS extension using TypeScript

Before I start, I just want to give credit to Tiago Pascal for his help in getting me started with some of the basics and tips and tricks that got me started with the JavaScript extensions and to Alexander Vanwynsberghe for a debugging tip that makes this all a lot easier. This post will give you a basic starting point for creating TFS Web Access JavaScript extension with most of it in proper TypeScript. This post will give a brief overview of the whole process and supply enough to get you started. In later posts, I will go into the different sections in more detail and explain what bits of code do or how I found I was able to know that bits of code could work in the web access.

Files Required

First, you need a name for your extension, I have chosen to use B1n4ryD1g1t.Tfs.Extensions so whenever you see that, you will replace it with your name, but I will try and remember to point out the couple of places required. The easiest way I find to create these plugins is to create a new HTML Application with TypeScript, this provides a good starting point and also makes it more familiar to me as it’s a solution and will allow me to build the project and any TypeScript errors will be revealed to me through the normal error list window.

image

Create 2 files (and remember to do the name replacing here :)), manifest.xml and B1n4ryD1g1t.Tfs.Extensions.min.js and then rename the solutions current app.ts to be B1n4ryD1g1t.Tfs.Extensions.debug.ts. If you build the solution and also show all files in the solution folder, you will notice that there is now a B1n4ryD1g1t.Tfs.Extensions.debug.js and B1n4ryD1g1t.Tfs.Extensions.debug.js.map file, you can include those in your solution. The last step of setting up the solution is to remove the app.css, default.htm and web.config as we won’t be needing these. Next, you need to use NuGet to add a reference to JQuery to your project.

imageimage

Now right click on your jquery-{version}.js file and click on Search for TypeScript Typings… and install the jquery.TypeScript.DefinitelyTyped typing.

imageimage

Your solution should end up looking something like below, we aren’t going to physically reference these jquery files in our extension. They were purely used to get the typings.

image

What Goes into the manifest.xml

The mainifest.xml has the basic information for your extension that TFS will show on the extensions page in the web access.

image

The plug in node is self explanatory and for now, you will leave the module nodes intact (remember to replace the name though). The 2 modules below allow our extension to run on the task and portfolio boards.

XML
<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> 

What is the Minimum Needed for the B1n4ryD1g1t.Tfs.Extensions.debug.ts?

The minimum content for our extension will be plain Js and will leave 4 errors in our solution that we can ignore:

image

The content for the extension will look like below:

JavaScript
 /// <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

Open your debug.js file and copy all of its contents to the min.js file and minify it, the web essentials extension for Visual Studio will help with this as it’s as easy as ctrl + alt + x. Open the folder where your extension is and zip the debug.js, min.js and manifest.xml files.

image

Now, you will need to upload your extension into TFS. Browse to the TFS Server Home, click on the admin settings button and then on extensions tab. Click Install, browse for your newly created zip file and click ok. Now all you need to do is click enable and ok to the warning. Now just refresh any of the task boards or portfolio boards and you will see an alert from inside the extension.

imageimage

Making Changes to the Extension

As you can see, this is a very lengthy process to add the extension and none of that changes for checking for changes unless you use a trick that Alexander blogged about Debugging TFS Web Access Extensions. This will basically using fiddler tell your machine that when it requests the js file for your extension, it must use the version js file that is in your solution instead of the one from TFS, this will speed up development a lot as you can just save the file which will trigger Visual Studio to update the debug.js file which refreshing TFS will now be loaded. For the Editor, I used the values regex:http://labtfs01/_static/tfs/12/_scripts/TFS/.+//_plugins/.+/B1n4ryD1g1t.Tfs.Extensions.js with header:CachControl=no-cache and regex:http://labtfs01/_static/tfs/12/_scripts/TFS/.+//_plugins/.+/B1n4ryD1g1t.Tfs.Extensions.js with C:\c\r\Apps\TfsJsExtensions\TfsJsExtensions\B1n4ryD1g1t.Tfs.Extensions.debug.js. Similar to the blog post just adding a wild card for the debug/min difference in TFS.

Adding Some Real TypeScript

To the bottom of your .ts file, add the TypeScript code below (sorry about no full TS highlighting).

JavaScript
 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);
            });
        }
    }
} 

This is all the code we will need to do the magic, all that is left is to wire it up to the extension. This can be done by replacing the method below in the original snippet for the extension.

JavaScript
B1n4ryD1g1tTfsExtension.prototype.initialize = function () {
	var bdCore = new B1n4ryD1g1tModule.Core
                 (require1, exports, TFS, Core, TFS_OM, TFS_WorkItemTracking);
	bdCore.init();
}; 

If you fresh your page now and you have the debug tip running, you will see that you have IDs on your work items and on the task board, the requirement states are showing.

Before:

imageimage

After:

imageimage

Hope this sparks something in others as it has in me. As said in the introduction, I will be extending what is covered in this article in later posts to provide more details.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect SSW
South Africa South Africa

Comments and Discussions

 
QuestionGood article Pin
dungle3023-Dec-13 20:45
dungle3023-Dec-13 20:45 
Thanks for really good tutorial with code sample. I've been looking for this guide quite sometime
/DL

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.