{"id":37217,"date":"2017-11-13T16:48:28","date_gmt":"2017-11-13T16:48:28","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/devops\/?p=37217"},"modified":"2019-02-14T15:50:34","modified_gmt":"2019-02-14T23:50:34","slug":"creating-work-item-form-extensions-in-visual-studio-team-services","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/devops\/creating-work-item-form-extensions-in-visual-studio-team-services\/","title":{"rendered":"Creating work item form extensions in Visual Studio Team Services"},"content":{"rendered":"<p><a href=\"https:\/\/visualstudio.com\/team-services\">Visual Studio Team Services<\/a> (VSTS) and its on-prem version TFS has an extensibility framework that lets 3rd party developers write and publish their own extensions. A VSTS extension is just a set of contributions where each contribution can contribute to certain contribution points provided by VSTS like hubs, pivots, menu items, work item forms, and more.<\/p>\n<p>This blog describes how to write efficient work item form extensions that can contribute to a work item form page, group, or control. <a href=\"https:\/\/docs.microsoft.com\/en-us\/vsts\/extend\/get-started\/node\">General documentation for writing extensions <\/a>are published in Microsoft docs, which also cover <a href=\"https:\/\/docs.microsoft.com\/en-us\/vsts\/extend\/develop\/add-workitem-extension\">how to write work item form extensions<\/a>. In this post, I provide more details on how to write well-performing work item form extensions.<\/p>\n<h3>How does the work item form work?<\/h3>\n<p>To understand the best time to run initialization logic for your contribution, let me first give you a little detail on how the work item form works. When a user opens a work item (either a standalone work item creation\/edit or from a query result grid), it creates the form based on the work item\u2019s type. If you open a \u201cBug\u201d work item, it will create a form instance for the \u201cBug\u201d type. This form instance will also load all the extension iframes after its creation. After that, if you open a \u201cFeature\u201d type, it will create a new form instance for this type and load new instances of the same contribution iframes in the form.<\/p>\n<p>Each work item type\u2019s form has its own instances of form contributions. These forms are cached by their work item types. If you open multiple \u201cBug\u201d work items, they will use the same form instance (and thus same contribution instances). For example \u2013 if you have a query that returns 2 Bugs and 2 Features, and you open Bug #1, the system will create a \u201cBug\u201d form. After that, if you open Bug #2 from the grid, it won\u2019t create a new instance of the same form, instead it will unload Bug #1 from the same form instance and load Bug #2 in it. If you open Feature #3 after this, it will create a new form instance for the Feature type (if it hadn\u2019t been created yet on the page) and load new instances of contributions in this new form.<\/p>\n<p>Understanding how the form is created is useful because many developers think that every time they open a new work item form, it will create new instances of contributions. They therefore place most of their initialize logic in the constructor of their contribution App. Instead of starting your contribution logic in the constructor, you should initialize the logic in the Work item form \u201conload\u201d event as \u00a0described in the next section.<\/p>\n<p>Note that this logic is true only in triage mode (when you open a work item form side by side with a query results grid). If you open a standalone work item (either in a dialog or in a full view), it creates a new instance of the form everytime, thus creating new instances of extension iframes. But in any case, initializing the logic of the contribution in the constructor is always bad, instead it should be initialized within the \u00a0\u201conLoad\u201d event.<\/p>\n<h3>Work item form contribution points<\/h3>\n<p>The work item form has 5 contribution points (excluding a menu contribution which is not discussed in this post) \u2013<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/form.png\" alt=\"\" width=\"1129\" height=\"649\" class=\"alignnone size-full wp-image-37175\" \/><\/p>\n<ol>\n<li>\n<h4>Page contribution<\/h4>\n<p>A page contribution contributes to the list of tabs (or pages) in the work item form \u2013\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/pagecontribution.png\" alt=\"\" width=\"524\" height=\"66\" class=\"alignnone size-full wp-image-37185\" \/><\/p>\n<p style=\"text-align: left\">In the screenshot above, \u201cRelated work items\u201d tab is a contributed tab. When users click this tab, it will load the contribution iframe in the form below the tab. Note that the contribution is not loaded into the form until the user clicks the tab.<\/p>\n<\/li>\n<li>\n<h4>Group contribution<\/h4>\n<p>A group contribution contributes to the grid of groups in the form \u2013\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/groupcontribution.png\" alt=\"\" width=\"872\" height=\"365\" class=\"alignnone size-full wp-image-37195\" \/>\nThe system loads a group contribution as soon as it renders the tab that contains the.<\/li>\n<li>\n<h4>Control contribution<\/h4>\n<p>A control contribution is placed inside a group. Similar to group contributions, a control contribution is also not loaded until the tab that contains the contribution is rendered.<\/li>\n<li>\n<h4>Menu contribution<\/h4>\n<p>A menu contribution contributes to the work item form\u2019s menu bar. These contributions fall under the ellipsis menu item.<\/li>\n<li>\n<h4>Observer contribution<\/h4>\n<p>If you want a contribution to not render on the form, but still be able to interact with the form, then you use the observer contribution. This contribution is initialized as a hidden iframe in the form. It\u2019s not visible to the user but it can still interact with the form using the services provided (as mentioned below).<\/li>\n<\/ol>\n<h3>Work item form extension services<\/h3>\n<p>Two services \u2013 <i>IWorkItemNotificationListener<\/i> and <i>WorkItemFormService<\/i> support the ability for contributions to interact with the work item form. Using these services, a contribution can interact with a work item form in the following two ways: &#8211;<\/p>\n<h4>Listen to form events<\/h4>\n<p>Contributions can listen to form events via <em>IWorkItemNotificationListener<\/em>. A work item form contribution can register event handlers to certain work item events which will be called whenever the work item form fires those events. The events are as follows:<\/p>\n<ol>\n<li>\n<h5>onLoaded<\/h5>\n<p>Fired when a work item is loaded in the form, or when a contribution is initialized on an already loaded work item form. This is a good place to initialize your contribution\u2019s logic, because firing of this event means that a work item is ready to be used. When a work item form is opened, it first creates the form (along with all its contributions) and then starts loading the work item data from the server. Once the work item data is loaded, the data is \u201cbound\u201d to the form and at this point it will fire the onLoaded event. Since the contributions are created during form creation, it is not guaranteed that a work item is ready in the form during their creation. So, trying to perform actions on the work item form as soon as the contribution is loaded won\u2019t work. All the work item form related activities should start from the point the onLoaded event is fired. Note that if you switch between work items of the same work item type in a query result grid, it will fire onLoaded event for each work item which is bound to the form. As mentioned above, when a new work item is opened in the form, it doesn\u2019t instantiate a new instance of contribution, instead it uses the same form instance and just unbinds the previous work item and binds the new work item.<\/li>\n<li>\n<h5>onUnloaded<\/h5>\n<p>Fired when a work item is unloaded in the form. This is particularly useful when users switch between different work items in a query result grid. When users switch between work items of the same type, it will first fire an onUnloaded event on the form for the old work item and then fire an onLoaded event on the form for the new work item. This event is a good place to dispose the internals of a contribution.<\/li>\n<li>\n<h5>onSaved<\/h5>\n<p>Fired when a work item is saved by the user on the form.<\/li>\n<li>\n<h5>onRefreshed<\/h5>\n<p>Fired when a work item is refreshed from the form.<\/li>\n<li>\n<h5>onReset<\/h5>\n<p>Fired when a work item is reset in the form.<\/li>\n<li>\n<h5>onFieldChanged<\/h5>\n<p>Fired whenever a field is changed on the form. This event w fires whenever the field value changes \u2013 either manually, by some work item rule, or by calling a function on the WorkItemFormService.<\/li>\n<\/ol>\n<p>More information on these events are at <a href=\"https:\/\/www.visualstudio.com\/en-us\/docs\/integrate\/extensions\/develop\/add-workitem-extension#listenforevents\">Extend the work item form<\/a><span>.<\/span><\/p>\n<h4>Perform actions on work item form<\/h4>\n<p>The <a href=\"https:\/\/www.visualstudio.com\/en-us\/docs\/integrate\/extensions\/reference\/client\/api\/tfs\/workitemtracking\/services\/workitemformservice\">Work Item Form Service<\/a> allows contributions to perform certain actions on the form. This service is available with all contribution types, but only works when a work item form is active on a page. The functions exposed by this service are documented in the link above, so I am not going to explain each of them in details. I will point out some interesting facts about some of the functions.<\/p>\n<ol>\n<li>\n<h5>Save()<\/h5>\n<p>Calling save() method on the form service saves the current state of the work item in the form (if it\u2019s in a valid state). This function returns an empty promise which is resolved if the save succeeds and is rejected if the save fails. If the work item in the form has not been changed at all, or if the work item in in an invalid state, this function would be no-op and the promise would neither resolve nor fail.<\/li>\n<li>\n<h5>setFieldValue()<\/h5>\n<p>Calling setFieldValue() will update the value for a field on the form and mark the work item as dirty. It will also fire onFieldChanged event on the <em>IWorkItemNotificationListener <\/em>object registered by the contribution. Keep in mind that any field change by the contribution would also fire a field changed event on the contribution. This information is useful because you don\u2019t want to end up in an infinite loop where on a field change event, the contribution sets a field value which would again fire the event and so on.<\/li>\n<\/ol>\n<p>All 5 kinds of work item form contributions are similar except for how they are rendered on the form. All of them can interact with the form using these two services.<\/p>\n<h3>A simple work item form group extension<\/h3>\n<p>I am going to demonstrate by using as an example a real custom group extension that is on the marketplace (link). This form group extension is built upon node using npm, typescript 2.3 (using typescript async await) and react. The source code for this extension is <a href=\"https:\/\/github.com\/mohitbagra\/vsts-extensions-checklist\">here<\/a>. Although the extension can be written in javascript, I highly recommend using typescript to avoid any runtime errors as most of the errors would be caught at build time in typescript. Microsoft releases typescript declarations of its internal classes, services and interfaces every 3 weeks which can be used if your extension needs to call VSTS rest APIs or Work item. The sdk can be found <a href=\"https:\/\/github.com\/Microsoft\/vss-web-extension-sdk\">here<\/a>.\nI\u2019ll assume that readers know how create the initial structure of VSTS extension and jump into some best practices on writing form contributions. If not, then please look into samples provided by VSTS <a href=\"https:\/\/github.com\/Microsoft\/vsts-extension-samples\/tree\/master\/work-item-form\">here<\/a><\/p>\n<p>The checklist group extension start with the declaration of its manifest in the vss-extension.json file:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/contributions_eg2.png\" alt=\"\" width=\"482\" height=\"272\" class=\"alignnone size-full wp-image-37436\" \/><\/p>\n<p>When the system loads a contribution, it loads an iframe targeting the starting html file. In this case, it\u2019s the index.html file.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/html_eg2.png\" alt=\"\" width=\"648\" height=\"422\" class=\"alignnone size-full wp-image-37456\" \/><\/p>\n<p>As soon as the html \u00a0loads, it\u2019ll call this javascript code. A couple of things to note here \u2013<\/p>\n<ol>\n<li>Authors can call VSS.init to configure the initialization of the extension. VSTS lets authors use some of its inbuilt controls and libraries in an extension, if \u201cusePlatformScripts\u201d is set to true. If authors also want to use css styles published by Microsoft, they can also choose to set \u201cusePlatformStyles\u201d to true. If the contribution wants to use its own styles and controls, then it can set them to false.<\/li>\n<li>When VSTS host page tries to load a contribution iframe, it sets a load timeout, and if the html page is not fully loaded until that timeout, the page will show an error in place of the extension. To avoid this, make sure that your html file is loaded as quickly as possible. Requiring all your javascripts and css files in the &lt;head&gt; of the html file will make loading the html slow because until all the resources are loaded, the page is not fully loaded. I suggest you lazy load your starting javascript module so that it doesn\u2019t block html render. That\u2019s why I load my \u201cscripts\/App\u201d javascript module using VSS.require which is just a wrapper over Requirejs.<\/li>\n<li>As soon as your module is ready, it needs to call VSS.notifyLoadSucceeded to notify the host page that contribution has successfully loaded.<\/li>\n<\/ol>\n<p>Let\u2019s see how the App module is defined.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/Constructor_eg2.png\" alt=\"\" width=\"599\" height=\"612\" class=\"alignnone size-full wp-image-37435\" \/><\/p>\n<p>When a contribution loads, there is no guarantee that a work item is bound to the form at that moment. Instead of initializing the internals of the contribution in the constructor of the main module, it should be initialized in an onLoaded() event and disposed of with an onUnloaded() event callback. This makes the contribution robust in scenarios when the user is switching between workitems in a query result grid.<\/p>\n<p>This is the crux of a simple form extension. A page contribution can be written like a group contribution. A control contribution is also the same except it can take some inputs from the user so they can configure the contribution per their needs.<\/p>\n<h3>Writing a custom control extension<\/h3>\n<p>A custom control can take some user inputs when a user tries to add it to a work item form. These user inputs are then passed to the control contribution when it\u2019s initialized, so that the contribution can initialize itself based on user input. A group or page contribution doesn\u2019t take any user input because they are meant to be standalone apps. The reason we let users pass input values to a custom control is that a custom control is primarily meant to act as a replacement of 1<sup>st<\/sup> party field controls on the form. For example, a custom control that replaces the HTML control provided by VSTS with a\u00a0\u00a0<a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=cschleiden.markdown\">markdown control<\/a>. Such a control can be used with any kind of HTML work item field, but only users can decide which field would it be used for.<\/p>\n<p>I created a simple Pattern custom control that restricts a field value to a certain pattern provided by a user (like an Email pattern or a Phone number pattern). The code for this extension is available at &#8211; <a href=\"https:\/\/github.com\/mohitbagra\/vsts-extensions-controllibrary\">https:\/\/github.com\/mohitbagra\/vsts-extensions-controllibrary<\/a>.<\/p>\n<p>This custom control binds itself to a string-based field (provided by the user) and restricts the field value to a certain pattern which is also provided by the user. If the value provided in that control doesn\u2019t match the specified pattern, it\u2019ll show an error. This control contribution can define its set of inputs in the extension\u2019s manifest \u2013<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/controlextension_manifest2.png\" alt=\"\" width=\"1074\" height=\"913\" class=\"alignnone size-full wp-image-37445\" \/><\/p>\n<p>In this example, the pattern control contribution needs 3 inputs from a user. Each input is described by its descriptor where authors can describe the type, name and other properties of the inputs. These descriptors are used to validate the input value provided by the user. For example,\u00a0 if an input is of type \u201cnumber\u201d, and the user tries to enter a string value for it, it won\u2019t let the user do it.<\/p>\n<p>The inputs described in this example are \u2013<\/p>\n<ol>\n<li>A string-based field which should be bound to this control. Notice that the type of this input is \u201cWorkItemField\u201d which means that users won\u2019t be able to enter any other value to this input. The type of field can also be defined by using \u201cworkItemFieldTypes\u201d so that user can only provide field of certain specified types.<\/li>\n<li>A string pattern which the control should allow as the field value.<\/li>\n<li>A custom error message if the user provides a value to the control which doesn\u2019t match the pattern.<\/li>\n<\/ol>\n<p>A control contribution is loaded like a group or page contribution. The difference is that the control contribution will get the input value provided by a user &#8211;<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/getInputs2.png\" alt=\"\" width=\"391\" height=\"58\" class=\"alignnone size-full wp-image-37455\" \/><\/p>\n<p>This will return an object where keys are the input ids \u2013 as defined in the extension manifest and values would be the values provided by the user. In this example, it would look like \u2013<\/p>\n<p>{ \u201cFieldName\u201d: \u201cTest.PatternField\u201d, \u201cPattern\u201d: \u201c&lt;pattern&gt;\u201d, \u201cErrorMessage\u201d: \u201c&lt;error message&gt;\u201d }<\/p>\n<p>To initialize the control, register a work item notification listener which listens to work item events \u2013<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/registerevents2.png\" alt=\"\" width=\"621\" height=\"251\" class=\"alignnone size-full wp-image-37475\" \/><\/p>\n<p>Depending on the need, the control contribution can listen to other events too like onSaved, onRefreshed etc. But in this example, we only need to listen to 3 events \u2013<\/p>\n<ol>\n<li>onLoaded \u2013 So that the contribution can refresh the field value in its control when user loads a different work item in the form<\/li>\n<li>onUnloaded \u2013 So that the contribution can clear the control\u2019s value<\/li>\n<li>onFieldChanged \u2013 So that the control refreshes its value whenever the value of the field it\u2019s bound to changes. This can be either manually by user or via some work item type rule. The onFieldChanged event is fired for every field value change, but the control needs to only worry about its own field, that\u2019s why we check that the field this control is bound to is in the arguments of field changed event.<\/li>\n<\/ol>\n<p>A field bound custom control needs 2 things to work \u2013 listen to work item\u2019s field changes and refresh its control to show the current field value, and listen to user changes in its own control and change the work item\u2019s field value.<\/p>\n<p>Both onLoaded and onFieldChanged callbacks call _invalidate() function which sets the value of the control by reading the current field value of the work item.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/invalidate2.png\" alt=\"\" width=\"598\" height=\"424\" class=\"alignnone size-full wp-image-37465\" \/><\/p>\n<p>To read the field value of the workitem, we use the WorkItemFormService\u2019s getFieldValue function. Once you read the value, you can set it to the custom control. In this case, I call a _setValue() function which just sets the value in an input box and also validates the input based on the pattern provided by user.<\/p>\n<p>To set the work item\u2019s field value based on user input into the custom control, we can use WorkItemFormService\u2019s setFieldValue function \u2013<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/fieldvaluechanged2.png\" alt=\"\" width=\"628\" height=\"327\" class=\"alignnone size-full wp-image-37447\" \/><\/p>\n<p>In this example, the onValueChanged function is bound to the \u201cinput\u201d event of the text box shown in the control.<\/p>\n<p>Note that both _invalidate and onValueChanged functions reads\/writes a variable \u201c_flushing\u201d. This it because the control can set field value as well as listen to field value changes. When this control sets the field value, it\u2019ll still get onFieldChanged event and we don\u2019t want to come across an infinite loop where the control first sets the field value, then gets the field changed event and then tries to refresh the control\u2019s value based on the same field value that it changed originally. This can be avoided by maintaining a private boolean variable here.<\/p>\n<h3>Auto resize group and control contribution iframe in the form<\/h3>\n<p>Unlike work item form page contributions and hub contributions, control and group contributions on work item form only get limited space for them to render. If you look at the screenshot at the top of this post, you can see how small the group and control contributions are. By default, the work item form gives only 70px height to control contributions and 300px height to group contributions. Since the form is responsive, the width of groups and controls depend upon how much space does the whole form have. Although the initial height of controls and groups are set, the form listens to certain resize events from the contributions and it resizes the parent host of the contributions to fit it.<\/p>\n<p>To change the height of a group or control contribution, the contribution can call VSS.resize function and give it appropriate size in pixels \u2013<\/p>\n<p><em>VSS.resize(width, height);<\/em><\/p>\n<p>When VSS.resize is called from a contribution, it resizes the parent iframe of the contribution on the work item form to resize to the height passed to this function. Note that work item form only respects the height parameter passed to this function and will ignore the width parameter. Also, if the value of height parameter is passed as null, the form will try to automatically resize the iframe to fit its current contents.<\/p>\n<p>For example, if you have a checklist group extension which allows user to add checklist items to it, the contribution would want to expand its height as more and more items are added. In this case, whenever a new item is added, the contribution can call \u2013<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/resize23.png\" alt=\"\" width=\"694\" height=\"83\" class=\"alignnone size-full wp-image-37485\" \/><\/p>\n<p>This function will call resize with height as contribution\u2019s body height. As more and more items are added, the contribution\u2019s body height will increase, and calling VSS.resize with the body height will expand the contribution\u2019s iframe in the form too.<\/p>\n<p>You can also hook this function as a callback to window resize event, so that whenever the browser window or the form is resized, the contribution can automatically resize itself.<\/p>\n<p>Note that, the contribution should always have a max height attribute in its body element, so that it doesn\u2019t expand too much and make the form look bad.<\/p>\n<p>Also note that if the contribution is making use of popups like a popup context menu, they will still be shown inside the contribution iframe and get cut off by the end of the frame.. To fix this, you can again make use of VSS.resize whenever you show or hide the popup.<\/p>\n<h3>Performance tips<\/h3>\n<p>As I mentioned above, the system sets a timeout for every contribution it tries to load. If the contribution and all its blocking resources (html, css, javascripts) are not loaded until the timeout hits, the system will show an error message in place of the contribution.\u00a0 To prevent your extensions from hitting this error, here are some tips on how to make the contribution load faster:<\/p>\n<ol>\n<li>Do not add all your scripts in the html\u2019s &lt;head&gt;. Instead make use of async javascript load using requireJs.<\/li>\n<li>Note that the system (VSTS\/TFS) can provide some 3<sup>rd<\/sup> party libraries like jquery, react, react dom, requireJs and Q on demand to the contribution. Contributions don\u2019t need to add these libraries in their html. If you are using AMD style javascript load in your app, you can just add import (or require) statements to these libraries and VSTS will load them for you. If you use typescript to write your app, you can use typescript declarations for these libraries for your dev work, but no need to actually make these libraries a part of your app.<\/li>\n<li>Make use of bundling tools like webpack. Bundling and minifying can really improve the javascript loading time if your app has many javascript files. I personally make heavy use of webpack and some of its plugins like uglifyJs, commonChunksPlugin and css-loaders and it speeds up the app load time greatly.<\/li>\n<li>If your App doesn\u2019t make use of any VSS platform javascript modules or VSS platform css styles, you don\u2019t need to load them from your App. In this case you can just set \u201cusePlatformScripts\u201d and \u201cusePlatformStyles\u201d to false in the code snippet above. If you set \u201cusePlatformStyles\u201d to true, it\u2019ll load a couple of css files and fonts from VSS which you can use in your contribution app. But if you are using your own styles, you can set this to false and it won\u2019t load VSS styles which can save you some load time.<\/li>\n<li>If your app uses a lot of VSS controls or libraries, you can add import or require statements to those scripts in your app and they will be loaded from VSTS page on demand. But these VSS modules will not be bundled when they are loaded. Even loading a single control like Combo from VSS actually loads a few other script modules individually which can really make the app slow. To improve this, add a VSS.require statement in your app\u2019s html file and pass in VSS module names to it which are getting used in your app. If you pass in multiple modules in VSS.require(), it won\u2019t load all of them individually, instead it will load them in one single bundle.<\/li>\n<\/ol>\n<p>Example &#8211;<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2017\/11\/html_eg2.png\" alt=\"\" width=\"648\" height=\"422\" class=\"alignnone size-full wp-image-37456\" \/><\/p>\n<p>I hope this blog post has provided you with some new insights to VSTS extensions. I have published 5 extensions to VSTS and the source code for all of them are on my <a href=\"https:\/\/github.com\/mohitbagra\/\"><\/a>. Pretty much all of them make use of React, Typescript 2.3+, Typescript async await, Office Fabric UI and webpack. You are welcome to use those repos as examples. I also have published a <a href=\"https:\/\/www.npmjs.com\/package\/vsts-extension-react-widgets\">npm<\/a> package which has some common react components which be used in a VSTS extension.<\/p>\n<p>Some helpful links \u2013<\/p>\n<ol>\n<li><a href=\"https:\/\/github.com\/Microsoft\/vsts-extension-samples\/tree\/master\/work-item-form\">VSTS sample extensions<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/en-us\/vsts\/extend\/get-started\/node\">Write your first extension for VSTS<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/en-us\/vsts\/extend\/develop\/add-workitem-extension\">Extend the work item form<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Microsoft\/vss-web-extension-sdk\">VSS library typescript typings<\/a><\/li>\n<\/ol>\n<p>Mohit Bagra<\/p>\n<p><a href=\"https:\/\/www.linkedin.com\/in\/mohit-bagra-3052028a\/\">LinkedIn<\/a>, <a href=\"https:\/\/twitter.com\/mohitbagra\">Twitter<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Visual Studio Team Services (VSTS) and its on-prem version TFS has an extensibility framework that lets 3rd party developers write and publish their own extensions. A VSTS extension is just a set of contributions where each contribution can contribute to certain contribution points provided by VSTS like hubs, pivots, menu items, work item forms, and [&hellip;]<\/p>\n","protected":false},"author":243,"featured_media":45953,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-37217","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"acf":[],"blog_post_summary":"<p>Visual Studio Team Services (VSTS) and its on-prem version TFS has an extensibility framework that lets 3rd party developers write and publish their own extensions. A VSTS extension is just a set of contributions where each contribution can contribute to certain contribution points provided by VSTS like hubs, pivots, menu items, work item forms, and [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/posts\/37217","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/users\/243"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/comments?post=37217"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/posts\/37217\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/media\/45953"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/media?parent=37217"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/categories?post=37217"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/devops\/wp-json\/wp\/v2\/tags?post=37217"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}