Skip to content

Commit

Permalink
Init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mmanela committed May 23, 2016
0 parents commit 161825a
Show file tree
Hide file tree
Showing 23 changed files with 5,585 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
node_modules
scripts/*.js
*.vsix
9 changes: 9 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,9 @@
{
"files.exclude": {
"**/*.js": {
"when": "$(basename).ts"
},
"**/*.vsix": true,
"*.gitignore": true
}
}
18 changes: 18 additions & 0 deletions .vscode/tasks.json
@@ -0,0 +1,18 @@
// A task runner configuration.
{
"version": "0.1.0",
"command": "grunt",
"isShellCommand": true,
"tasks": [
{
"taskName": "build",
"isBuildCommand": true,
"problemMatcher": "$msCompile"
},
{
"taskName": "publish",
"isBuildCommand": false,
"problemMatcher": "$msCompile"
}
]
}
3 changes: 3 additions & 0 deletions details.md
@@ -0,0 +1,3 @@
## vsts-workitem-recentlyviewed ##

Adds a group to the work item form showing who recently viewed the work item
33 changes: 33 additions & 0 deletions fullView.html
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<script src="scripts/VSS.SDK.min.js"></script>
<script src="scripts/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="styles/RecentlyViewed.css">
</head>

<body>
<script type="text/javascript">
// Initialize framework
VSS.init({
explicitNotifyLoaded: true,
usePlatformScripts: true,
configureModuleLoader: true
});

// Load main entry point for extension
VSS.require(["scripts/RecentlyViewed"], function (RecentlyViewed) {
// Loading succeeded
var recentlyViewedGroupView = new RecentlyViewed.RecentlyViewedGroupView();
recentlyViewedGroupView.initialize();

VSS.notifyLoadSucceeded();
});
</script>

<div class="rv-full"></div>
</body>

</html>
49 changes: 49 additions & 0 deletions gruntfile.js
@@ -0,0 +1,49 @@
module.exports = function (grunt) {
grunt.initConfig({
ts: {
build: {
src: ["scripts/**/*.ts"],
tsconfig: true
},
options: {
fast: 'never'
}
},
exec: {
package: {
command: "tfx extension create --manifest-globs vss-extension.json",
stdout: true,
stderr: true
},
publish: {
command: "tfx extension publish --service-url https://marketplace.visualstudio.com --manifest-globs vss-extension.json",
stdout: true,
stderr: true
}
},
copy: {
scripts: {
files: [{
expand: true,
flatten: true,
src: ["node_modules/vss-web-extension-sdk/lib/VSS.SDK.min.js", "node_modules/moment/min/moment.min.js"],
dest: "scripts",
filter: "isFile"
}]
}
},

clean: ["scripts/**/*.js", "*.vsix"]
});

grunt.loadNpmTasks("grunt-ts");
grunt.loadNpmTasks("grunt-exec");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks('grunt-contrib-clean');

grunt.registerTask("build", ["ts:build", "copy:scripts"]);
grunt.registerTask("package", ["build", "exec:package"]);
grunt.registerTask("publish", ["default", "exec:publish"]);

grunt.registerTask("default", ["package"]);
};
Binary file added img/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions package.json
@@ -0,0 +1,19 @@
{
"devDependencies": {
"grunt": "~0.4.5",
"grunt-cli": "^0.1.13",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-copy": "~0.8.2",
"grunt-exec": "~0.4.6",
"tfx-cli": "^0.3.13",
"tsd": "~0.6.5",
"vss-web-extension-sdk": "^1.95.2",
"moment": "*"
},
"name": "vsts-workitem-recentlyviewed",
"private": true,
"version": "0.0.0",
"dependencies": {
"grunt-ts": "^5.3.2"
}
}
41 changes: 41 additions & 0 deletions readme.md
@@ -0,0 +1,41 @@
## vsts-extension-workitem-activities ##

VSTS Extension adds 'Activities' hub under Work group hub providing access to recent activites for work items

This extension was boostrapped from https://cschleiden.wordpress.com/2016/02/24/extending-vsts-setup/.

### Structure ###

```
/scripts - Typescript code for extension
/img - Image assets for extension and description
/typings - Typescript typings
details.md - Description to be shown in marketplace
index.html - Main entry point
vss-extension.json - Extension manifest
```

### Usage ###

1. Clone the repository
1. `npm install` to install required dependencies
2. `grunt` to build and package the application

#### Grunt ####

Three basic `grunt` tasks are defined:

* `build` - Compiles TS files in `scripts` folder
* `package` - Builds the vsix package
* `publish` - Publishes the extension to the marketplace using `tfx-cli`

Note: To avoid `tfx` prompting for your token when publishing, login in beforehand using `tfx login` and the service uri of ` https://app.market.visualstudio.com`.

#### Including framework modules ####

The VSTS framework is setup to initalize the requirejs AMD loader, so just use `import Foo = require("foo")` to include framework modules.

#### VS Code ####

The included `.vscode` config allows you to open and build the project using [VS Code](https://code.visualstudio.com/).
33 changes: 33 additions & 0 deletions recentlyViewedGroup.html
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<script src="scripts/VSS.SDK.min.js"></script>
<script src="scripts/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="styles/RecentlyViewed.css">
</head>

<body>
<script type="text/javascript">
// Initialize framework
VSS.init({
explicitNotifyLoaded: true,
usePlatformScripts: true,
configureModuleLoader: true
});

// Load main entry point for extension
VSS.require(["scripts/RecentlyViewed"], function (RecentlyViewed) {
// Loading succeeded
var recentlyViewedGroupView = new RecentlyViewed.RecentlyViewedGroupView();
recentlyViewedGroupView.initialize();

VSS.notifyLoadSucceeded();
});
</script>

<div class="rv-group"></div>
</body>

</html>
106 changes: 106 additions & 0 deletions scripts/Controls.ts
@@ -0,0 +1,106 @@
/// <reference path='../typings/moment/moment.d.ts' />

import Q = require("q");
import VSS_Service = require("VSS/Service");
import * as Utils_Core from "VSS/Utils/Core";
import {Control} from "VSS/Controls";
import {StatusIndicator} from "VSS/Controls/StatusIndicator";
import {CollapsiblePanel} from "VSS/Controls/Panels";
import * as WitClient from "TFS/WorkItemTracking/RestClient";
import {WorkItemUpdate} from "TFS/WorkItemTracking/Contracts";
import {WorkItemFormNavigationService} from "TFS/WorkItemTracking/Services";
import { WorkItemVisit, IdentityReference, Constants} from "scripts/Models";
import {manager} from "scripts/observer"


export interface IRecentlyViewedListOptions {
maxCount: number;
}

export class RecentlyViewedList extends Control<IRecentlyViewedListOptions> {
private _visitsContainer: JQuery;
private _currentWorkItemId: number;

public initialize(): void {
super.initialize();

// Initialize elements
this._visitsContainer = $("<div/>").addClass("rv-container").appendTo(this.getElement());
}

public initializeOptions(options: IRecentlyViewedListOptions) {
this._options = options;
}

/*
* Renders views
*
*/
public render(workItemId: number, visits: WorkItemVisit[]): void {

// If we already rendered this work item no-op
if(workItemId === this._currentWorkItemId) {
return;
}

this._currentWorkItemId = workItemId;
this._visitsContainer.empty();

if (visits && visits.length > 0) {


// Pre-process the list to ensure that in the list views
// we show an optimize version given the count limit
// This entails trying to show unique values over repeated views

let map = {};
let uniqueVisits = visits.reverse().filter((visit) =>{
if(map[visit.user.uniqueName] === undefined) {
map[visit.user.uniqueName] = 0;
return true;
} else {
map[visit.user.uniqueName] += 1;
return false;
}

});

uniqueVisits = uniqueVisits.slice(0,this._options.maxCount);

// Render filtered activities
uniqueVisits.forEach((visit: WorkItemVisit) => {
this._visitsContainer.append(this._createVisitRow(visit));
});
}
}

private _createVisitRow(visit: WorkItemVisit): JQuery {
var $result = $("<div />").addClass("rv-visit");

// Image/Person
var identityImageUrl = `${VSS.getWebContext().host.uri}/_api/_common/IdentityImage?id=`;
identityImageUrl = `${identityImageUrl}&identifier=${visit.user.uniqueName}&identifierType=0`;
var $visitedImageElement = $("<div/>").addClass("visited-image");
var $imageElement = $("<img/>").attr("src", identityImageUrl).appendTo($visitedImageElement);
$result.append($visitedImageElement);


var $userNameElement = $("<div class='visited-name' />").append(visit.user.name);
$userNameElement.attr("title", visit.user.uniqueName);
$imageElement.attr("title", visit.user.name);
$result.append($userNameElement);




// Visited date
var dateMoment = moment(visit.date.toLocaleString());
var dateStringFromNow = dateMoment.fromNow();
var dateElem = $("<div />").addClass("visited-date").text(`${dateStringFromNow}`);
dateElem.attr("title", dateMoment.format());
dateElem.appendTo($result);

return $result;
}

}
30 changes: 30 additions & 0 deletions scripts/Models.ts
@@ -0,0 +1,30 @@
export class WorkItemVisit {
public workItemId: number;
public revision: number;
public date: string;
public user: UserContext;
}

export class IdentityReference {
id: string;
displayName: string;
uniqueName: string;
isIdentity: boolean;
}

export class Constants {
public static StorageKey: string = "WorkItemVisits";
public static UtcRegex = /\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z/;

public static ExtensionPublisher = "mmanela";
public static ExtensionName = "vsts-workitem-recentlyviewed";

public static MaxVisitsToStore = 1000;

public static GroupViewVisitCount = 4;
}


export function getStorageKey(workItemId: number){
return `TEMP2-${Constants.StorageKey}-${workItemId}`;
}

0 comments on commit 161825a

Please sign in to comment.