Click here to Skip to main content
Click here to Skip to main content

Tagged as

Tfs Js Extension using TypeScript

, 29 Sep 2013
Rate this:
Please Sign up or sign in to vote.
Categories: TFS JS ExtensionTags: TFS 2013 RC, Tfs Js Extension, TFS Web AccessBefore 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 start with the JavaScript extensions and to Alexander Vanwynsberghe for

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 start 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 choose to use B1n4ryD1g1t.Tfs.Extensions so when ever you see that you will replace it with your name but I will try 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 Smile), 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 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 plugin 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.   

<WebAccess version="12.0">
  <plugin 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>
  </plugin>
</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    

 /// <reference path="Scripts/typings/jquery/jquery.d.ts" />
<pre>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 mainifest.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 it 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 you .ts file add the TypeScript code below (sorry about no full TS highlighting). 

 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.

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 intro I will be extending on what is covered in this article in later posts to provide more details.   

Downloads 

  • Download TfsJsExtensions_2013-09-27_06-55-45Z.zip - 625.8 KB
  • Download TfsJsExtensions.zip - 4 KB 
  • License

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

    Share

    About the Author

    Gordon W Beeming
    Software Developer Derivco
    South Africa South Africa
    Developer who loves pushing the limits and playing with amazing @Microsoft Tech, #TFS, ALM Rangers, MVP, FoRG.
     
    http://31og.com
    Follow on   Twitter   Google+   LinkedIn

    Comments and Discussions

     
    QuestionGood article Pinmemberdungle3023-Dec-13 20:45 

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

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

    | Advertise | Privacy | Mobile
    Web01 | 2.8.140827.1 | Last Updated 30 Sep 2013
    Article Copyright 2013 by Gordon W Beeming
    Everything else Copyright © CodeProject, 1999-2014
    Terms of Service
    Layout: fixed | fluid