1
0
Форкнуть 0
This commit is contained in:
Maria McLaughlin 2016-08-10 13:54:13 -07:00
Родитель c7aeea80f0
Коммит cb3fa8b916
24 изменённых файлов: 767 добавлений и 0 удалений

4
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
node_modules
scripts/*.js
typings
dist

18
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -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"
}
]
}

5
configs/dev.json Normal file
Просмотреть файл

@ -0,0 +1,5 @@
{
"id": "hitcount-control-dev",
"name": "Hitcount Control (dev)",
"public": false
}

3
configs/release.json Normal file
Просмотреть файл

@ -0,0 +1,3 @@
{
"public": true
}

3
details.md Normal file
Просмотреть файл

@ -0,0 +1,3 @@
# vsts-extension-ts-seed-simple #
Describe your extension here. This description will be shown in the marketplace. You can use *Markdown*.

84
gruntfile.js Normal file
Просмотреть файл

@ -0,0 +1,84 @@
module.exports = function (grunt) {
grunt.initConfig({
ts: {
build: {
tsconfig: true,
"outDir": "./dist/scripts"
},
buildTest: {
tsconfig: true,
"outDir": "./test/scripts",
src: ["./scripts/**/*.tests.ts"]
},
options: {
fast: 'never'
}
},
exec: {
package_dev: {
command: "tfx extension create --root dist --manifest-globs vss-extension.json --overrides-file configs/dev.json",
stdout: true,
stderr: true
},
package_release: {
command: "tfx extension create --root dist --manifest-globs vss-extension.json --overrides-file configs/release.json",
stdout: true,
stderr: true
},
publish_dev: {
command: "tfx extension publish --service-url https://marketplace.visualstudio.com --root dist --manifest-globs vss-extension.json --overrides-file configs/dev.json",
stdout: true,
stderr: true
},
publish_release: {
command: "tfx extension publish --service-url https://marketplace.visualstudio.com --root dist --manifest-globs vss-extension.json --overrides-file configs/release.json",
stdout: true,
stderr: true
}
},
copy: {
scripts: {
files: [{
expand: true,
flatten: true,
src: ["node_modules/vss-web-extension-sdk/lib/VSS.SDK.min.js"],
dest: "dist/scripts",
filter: "isFile"
},
{
expand: true,
flatten: false,
src: ["styles/**", "img/**", "*.html", "vss-extension.json", "*.md"],
dest: "dist"
}]
}
},
clean: ["scripts/**/*.js", "*.vsix", "dist", "test"],
karma: {
unit: {
configFile: 'karma.conf.js',
singleRun: true,
browsers: ["PhantomJS"]
}
}
});
grunt.loadNpmTasks("grunt-ts");
grunt.loadNpmTasks("grunt-exec");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-karma');
grunt.registerTask("build", ["ts:build", "copy:scripts"]);
grunt.registerTask("test", ["ts:buildTest", "karma:unit"]);
grunt.registerTask("package-dev", ["build", "exec:package_dev"]);
grunt.registerTask("package-release", ["build", "exec:package_release"]);
grunt.registerTask("publish-dev", ["package-dev", "exec:publish_dev"]);
grunt.registerTask("publish-release", ["package-release", "exec:publish_release"]);
grunt.registerTask("default", ["package-dev"]);
};

27
index.html Normal file
Просмотреть файл

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en" style="height:100%">
<head>
<meta charset="utf-8" />
<!-- VSS Framework -->
<script src="scripts/VSS.SDK.min.js"></script>
<title>Color Control</title>
<link rel="stylesheet" href="styles/style.css" type="text/css"/>
</head>
<body style="height:100%">
<script>
VSS.init({
explicitNotifyLoaded: true,
usePlatformScripts: true
});
// Load main entry point for extension
VSS.require(["scripts/app"], function () {
// loading succeeded
VSS.notifyLoadSucceeded();
});
</script>
</body>
</html>

58
karma.conf.js Normal file
Просмотреть файл

@ -0,0 +1,58 @@
// Karma configuration
// Generated on Thu Jun 30 2016 14:46:40 GMT-0700 (Pacific Daylight Time)
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'requirejs', 'chai'],
// list of files / patterns to load in the browser
files: [
{ pattern: 'test/**/*.js', included: false },
'test-main.js'
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}

29
package.json Normal file
Просмотреть файл

@ -0,0 +1,29 @@
{
"devDependencies": {
"chai": "^3.5.0",
"grunt": "~1.0.1",
"grunt-cli": "^1.2.0",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-copy": "~1.0.0",
"grunt-exec": "~0.4.7",
"grunt-karma": "^2.0.0",
"grunt-ts": "^5.5.1",
"karma": "^1.1.0",
"karma-chai": "^0.1.0",
"karma-mocha": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.1",
"karma-requirejs": "^1.0.0",
"mocha": "^2.5.3",
"requirejs": "^2.2.0",
"tfx-cli": "^0.3.13",
"typescript": "^1.8.10",
"typings": "^1.0.4",
"vss-web-extension-sdk": "^1.100.0"
},
"name": "vsts-extension-ts-seed-simple",
"private": true,
"version": "0.5.0",
"scripts": {
"postinstall": "typings install"
}
}

21
scripts/app.ts Normal file
Просмотреть файл

@ -0,0 +1,21 @@
/// <reference path="../typings/index.d.ts" />
import { Controller } from "./control";
import * as ExtensionContracts from "TFS/WorkItemTracking/ExtensionContracts";
var control: Controller;
var provider = () => {
return {
onLoaded: (workItemLoadedArgs: ExtensionContracts.IWorkItemLoadedArgs) => {
control = new Controller();
},
onFieldChanged: (fieldChangedArgs: ExtensionContracts.IWorkItemFieldChangedArgs) => {
var changedValue = fieldChangedArgs.changedFields[control.getFieldName()];
if (changedValue !== undefined) {
control.updateExternal(changedValue);
}
}
}
};
VSS.register("mariamclaughlin.hitcount-control-dev.hitcount-control-contribution", provider);

83
scripts/control.ts Normal file
Просмотреть файл

@ -0,0 +1,83 @@
/** The class control.ts will orchestrate the classes of InputParser, Model and View
* in order to perform the required actions of the extensions.
*/
import * as VSSService from "VSS/Service";
import * as WitService from "TFS/WorkItemTracking/Services";
import * as ExtensionContracts from "TFS/WorkItemTracking/ExtensionContracts";
import { Model } from "./model";
import { View } from "./view";
import { ErrorView } from "./errorView";
import * as Q from "q";
export class Controller {
private _fieldName: string = "";
private _inputs: IDictionaryStringTo<string>;
private _model: Model;
private _view: View;
constructor() {
this._initialize();
}
private _initialize(): void {
this._inputs = VSS.getConfiguration().witInputs;
this._fieldName = this._inputs["FieldName"];
WitService.WorkItemFormService.getService().then(
(service) => {
Q.spread(
[service.getFieldValue(this._fieldName)],
(currentValue: number) => {
// Dependent on view, model, and inputParser refactoring
this._model = new Model(Number(currentValue));
this._view = new View(this._model, (val) => {
this._updateInternal(val);
}, () => {
this._model.incrementValue();
this._updateInternal(this._model.getCurrentValue());
}, () => {
this._model.decrementValue();
this._updateInternal(this._model.getCurrentValue());
});
}, this._handleError
).then(null, this._handleError);
},
this._handleError);
}
private _handleError(error: string): void {
let errorView = new ErrorView(error);
}
private _updateInternal(value: number): void {
WitService.WorkItemFormService.getService().then(
(service) => {
service.setFieldValue(this._fieldName, value).then(
() => {
this._update(value);
}, this._handleError)
},
this._handleError
);
}
private _update(value: number): void {
this._model.setCurrentValue(Number(value));
this._view.update(value);
}
public updateExternal(value: number): void {
this._update(Number(value));
}
public getFieldName(): string {
return this._fieldName;
}
}

37
scripts/errorView.ts Normal file
Просмотреть файл

@ -0,0 +1,37 @@
/***************************************************************************
Purpose: This class is being used to get errors from an input parser and
a model. It takes all the errors and put them in an array in
order to be sent to a view to display them.
***************************************************************************/
// shows the errors in the control container rather than the control.
export class ErrorView {
constructor(error: string) {
// container div
var container = $("<div />");
container.addClass("container");
// create an icon and text for the error
var warning = $("<p />");
warning.text(error);
warning.attr("title", error);
container.append(warning);
// include documentation link for help.
var help = $("<p />");
help.text("See ");
var a = $("<a> </a>");
a.attr("href", "https://www.visualstudio.com/en-us/products/visual-studio-team-services-vs.aspx");
a.attr("target", "_blank");
a.text("Documentation.");
help.append(a);
container.append(help);
$('body').empty().append(container);
}
}

32
scripts/model.tests.ts Normal file
Просмотреть файл

@ -0,0 +1,32 @@
import { expect } from 'chai';
import { Model } from './model';
describe("Model", () => {
let model: Model;
beforeEach(() => {
model = new Model(0);
});
it("current value of 0", () => {
expect(model.getCurrentValue()).to.be.deep.equal(0);
});
it("next value from 0", () => {
model.incrementValue();
expect(model.getCurrentValue()).to.be.deep.equal(1);
});
it("previous value of 0", () => {
model.decrementValue();
expect(model.getCurrentValue()).to.be.deep.equal(0);
});
it("previous and previous value of 20 is 18", () => {
model.setCurrentValue(20);
model.decrementValue();
model.decrementValue();
expect(model.getCurrentValue()).to.be.deep.equal(18);
});
});

35
scripts/model.ts Normal file
Просмотреть файл

@ -0,0 +1,35 @@
export class Model {
/**
* Model takes the initial value from Control and sets it to the current value
* selected in the Hit Count custom control. This will be updated in View and
* changes as the user increments and decrements the value.
*/
constructor(initialValue: number) {
this._currentValue = initialValue;
}
private _currentValue: number;
public setCurrentValue(value: number) {
if (value === undefined) {
throw "Undefined value";
}
this._currentValue = value;
}
public decrementValue() {
if (this._currentValue > 0) {
this.setCurrentValue(this._currentValue - 1);
}
}
public incrementValue() {
this.setCurrentValue(this._currentValue + 1);
}
public getCurrentValue(): number {
return this._currentValue;
}
}

81
scripts/view.ts Normal file
Просмотреть файл

@ -0,0 +1,81 @@
/// <reference path="../typings/index.d.ts" />
import { Model } from "./model";
/**
* Class view returns the view of a the control rendered to allow
* the user to change the value.
*/
export class View {
private currentValue: string = "";
constructor(private model: Model, private onInputChanged: Function, private onUpTick: Function, private onDownTick: Function) {
this._init();
}
private _init(): void {
var container = $("<div />");
container.addClass("container combo input-text-box emptyBorder");
var hitcount = $("<input />").attr("type", "string");
hitcount.addClass("wrap");
container.append(hitcount);
this.currentValue = String(this.model.getCurrentValue());
hitcount.val(this.currentValue);
hitcount.attr("aria-valuenow", this.currentValue);
hitcount.change( () => {
this._inputChanged();
}).bind('keydown', (evt: JQueryKeyEventObject) => {
if (evt.keyCode == 38) {
if (this.onUpTick) {
this.onUpTick();
evt.preventDefault();
}
}
else if (evt.keyCode == 40) {
if (this.onDownTick) {
this.onDownTick();
evt.preventDefault();
}
}
});
var uptick = $("<div />");
uptick.click( () => {
this.onUpTick();
});
uptick.addClass("bowtie-icon bowtie-arrow-up");
var downtick = $("<div />");
downtick.click( () => {
this.onDownTick();
});
downtick.addClass("bowtie-icon bowtie-arrow-down");
container.append(downtick);
container.append(uptick);
$("body").append(container);
}
private _inputChanged(): void {
let newValue = $(".wrap").val();
if (this.onInputChanged) {
this.onInputChanged(newValue);
}
}
public update(value: number) {
this.currentValue = String(value);
$(".wrap").val("");
$(".wrap").val(this.currentValue);
}
}

29
styles/style.css Normal file
Просмотреть файл

@ -0,0 +1,29 @@
body {
font-size: 14px;
}
input:focus {
outline: none;
}
.bowtie-icon {
margin: 0 2px 2px 0;
padding: 5px;
float: right;
line-height: 1.5;
box-shadow: none;
}
input {
line-height: 1.8;
font-size: 16px;
border: none;
}
.wrap {
width: 60%;
}
.container:hover {
border: 1px solid lightgray;
}

27
test-main.js Normal file
Просмотреть файл

@ -0,0 +1,27 @@
var allTestFiles = [];
var TEST_REGEXP = /(spec|tests)\.js$/i;
// Get a list of all the test files to include
Object.keys(window.__karma__.files).forEach(function (file) {
if (TEST_REGEXP.test(file)) {
// Normalize paths to RequireJS module names.
// If you require sub-dependencies of test files to be loaded as-is (requiring file extension)
// then do not normalize the paths
var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '');
allTestFiles.push(normalizedTestModule);
}
})
require.config({
// Karma serves files under /base, which is the basePath from your config file
baseUrl: '/base',
paths: {
"chai": "node_modules/chai/chai"
},
// dynamically load all test files
deps: allTestFiles,
callback: window.__karma__.start
});

0
test/scripts/control.js Normal file
Просмотреть файл

24
test/scripts/errorView.js Normal file
Просмотреть файл

@ -0,0 +1,24 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var ErrorView = (function () {
function ErrorView(error) {
var container = $("<div />");
container.addClass("container");
var warning = $("<p />");
warning.text(error);
warning.attr("title", error);
container.append(warning);
var help = $("<p />");
help.text("See ");
var a = $("<a> </a>");
a.attr("href", "https://www.visualstudio.com/en-us/products/visual-studio-team-services-vs.aspx");
a.attr("target", "_blank");
a.text("Documentation.");
help.append(a);
container.append(help);
$('body').empty().append(container);
}
return ErrorView;
}());
exports.ErrorView = ErrorView;
});

30
test/scripts/model.js Normal file
Просмотреть файл

@ -0,0 +1,30 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var Model = (function () {
function Model(initialValue) {
this._currentValue = initialValue;
}
Model.prototype.setCurrentValue = function (value) {
if (value === undefined) {
throw "Undefined value";
}
this._currentValue = value;
};
Model.prototype.selectPreviousOption = function () {
if (this._currentValue > 0 && this._currentValue !== -1) {
this.setCurrentValue(this._currentValue - 1);
}
else {
this.setCurrentValue(0);
}
};
Model.prototype.selectNextOption = function () {
this.setCurrentValue(this._currentValue + 1);
};
Model.prototype.getCurrentValue = function () {
return this._currentValue;
};
return Model;
}());
exports.Model = Model;
});

Просмотреть файл

@ -0,0 +1,20 @@
define(["require", "exports", 'chai', './model'], function (require, exports, chai_1, model_1) {
"use strict";
describe("Model", function () {
var model;
beforeEach(function () {
model = new model_1.Model(0);
});
it("current value of 0", function () {
chai_1.expect(model.getCurrentValue()).to.be.deep.equal(0);
});
it("next value from 0", function () {
model.selectNextOption();
chai_1.expect(model.getCurrentValue()).to.be.deep.equal(1);
});
it("previous value of 0", function () {
model.selectPreviousOption();
chai_1.expect(model.getCurrentValue()).to.be.deep.equal(0);
});
});
});

9
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,9 @@
{
"compilerOptions": {
"module": "amd",
"sourceMap": false
},
"exclude": [
"node_modules"
]
}

11
typings.json Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"globalDependencies": {
"chai": "registry:dt/chai#3.4.0+20160601211834",
"jquery": "registry:dt/jquery#1.10.0+20160628074423",
"knockout": "registry:dt/knockout#0.0.0+20160512130947",
"mocha": "registry:dt/mocha#2.2.5+20160619032855",
"q": "registry:dt/q#0.0.0+20160613154756",
"tfs": "npm:vss-web-extension-sdk/typings/tfs.d.ts",
"vss": "npm:vss-web-extension-sdk/typings/vss.d.ts"
}
}

97
vss-extension.json Normal file
Просмотреть файл

@ -0,0 +1,97 @@
{
"manifestVersion": 1,
"id": "hitcount-control",
"version": "0.1.0",
"name": "HitCount Control",
"scopes": [
"vso.work",
"vso.work_write"
],
"description": "Describe your extension.",
"publisher": "<your-publisher",
"icons": {
},
"targets": [
{
"id": "Microsoft.VisualStudio.Services"
}
],
"tags": [
"Sample"
],
"content": {
"details": {
"path": "details.md"
}
},
"links": {
"home": {
"uri": "https://bit.ly"
},
"getstarted": {
"uri": "https://bit.ly"
},
"learn": {
"uri": "https://bit.ly"
},
"support": {
"uri": "https://bit.ly"
},
"repository": {
"uri": "https://bit.ly"
},
"issues": {
"uri": "https://bit.ly"
}
},
"branding": {
"color": "rgb(220, 235, 252)",
"theme": "light"
},
"files": [
{
"path": "img",
"addressable": true
},
{
"path": "scripts",
"addressable": true
},
{
"path": "styles",
"addressable": true
},
{
"path": "index.html",
"addressable": true
}
],
"categories": [
"Integrate"
],
"contributions": [
{
"id": "hitcount-control-contribution",
"type": "ms.vss-work-web.work-item-form-control",
"targets": [
"ms.vss-work-web.work-item-form"
],
"properties": {
"name": "Priority",
"group": "contributed",
"uri": "index.html",
"height": 28,
"inputs": [
{
"id": "FieldName",
"description": "The field associated with the control.",
"validation": {
"dataType": "Field",
"isRequired": true
}
}
]
}
}
]
}