This commit is contained in:
Maria McLaughlin 2016-08-02 16:00:23 -07:00
Коммит b3f888b492
44 изменённых файлов: 2095 добавлений и 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": "color-control-dev",
"name": "Color Form 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"]);
};

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

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="dist/scripts/view.js"></script>
</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/app"], function () {
// Loading succeeded
VSS.notifyLoadSucceeded();
});
</script>-->
</body>
</html>

Двоичные данные
img/logo.JPG Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 28 KiB

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

Двоичные данные
mariamclaughlin.color-control-dev-0.1.0.vsix Normal file

Двоичный файл не отображается.

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

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

@ -0,0 +1,50 @@
## vsts-extension-ts-seed-simple ##
Very simple seed project for developing VSTS extensions using Typescript. Utilizes Typescript, grunt, and tsd. Detailed explanation how to get started can be found at 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 local dependencies
2. `npm install -g grunt` to install a global copy of grunt (unless it's already installed)
2. `grunt` to build and package the application
#### Grunt ####
Three basic `grunt` tasks are defined:
* `build` - Compiles TS files in `scripts` folder
* `package-dev` - Builds the development version of the vsix package
* `package-release` - Builds the release version of the vsix package
* `publish-dev` - Publishes the development version of the extension to the marketplace using `tfx-cli`
* `publish-release` - Publishes the release version of 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://marketplace.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/).
#### Unit Testing ####
The project is setup for unit testing using `mocha`, `chai`, and the `karma` test runner. A simple example unit test is included in `scripts/logic/messageHelper.tests.ts`. To run tests just execute:
```
grunt test
```

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

@ -0,0 +1,5 @@
export interface IOption {
value: string,
color: string,
label: string
}

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

@ -0,0 +1,166 @@
import { expect } from 'chai';
import { InputParser } from "./InputParser";
import { IOption } from "./IOption"
describe("InputParser", () => {
const bestCaseDict: IDictionaryStringTo<string> = {
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "0;1;2;3",
"Labels": "Critical;High;Medium;Low"
};
const bestCaseValues = ["1", "2", "3", "4"];
it("gets the field name specified in dictionary", () => {
expect(InputParser.getFieldName(bestCaseDict)).to.be.deep.equal("Priority");
});
it("throws when field name not specified", () => {
expect(() => InputParser.getFieldName({
"FieldName": ""
})).throw("FieldName not specified.");
});
it("returns an array of interfaces", () => {
expect(InputParser.getOptions(bestCaseDict, bestCaseValues)).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("returns options with empty strings in label key when no labels are provided", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": ""
}, bestCaseValues)).to.be.deep.equal([
{ value: "1", color: "red", label: "" },
{ value: "2", color: "orange", label: "" },
{ value: "3", color: "yellow", label: "" },
{ value: "4", color: "blue", label: "" }]);
});
it("returns 1 default color when 1 value and no colors are provided", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "",
"Values": "1",
"Labels": "Critical"
}, ["1"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" }]);
});
it("returns options with default colors and NO labels when NO colors and NO labels provided.", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "",
"Values": "1;2;3;4",
"Labels": ""
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "" },
{ value: "2", color: "orange", label: "" },
{ value: "3", color: "yellow", label: "" },
{ value: "4", color: "blue", label: "" }]);
});
it("throws when allowed values are not specified", () => {
expect(() => InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "",
"Labels": "Critical;High;Medium"
}, [])).throw("Allowed values not specified.");
});
it("Returns options with some empty labels if less labels than values provided", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "" }]);
});
it("throws when less colors than values are provided", () => {
expect(() => InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low"
}, ["1", "2", "3", "4"])).throw("Not enough colors provided in admin XML file.");
});
it("gives one label to every value, and truncates unused labels when MORE Labels THAN values are provided", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low;Very Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("gives one color to every value, and truncates unused colors when MORE colors THAN values are provided", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue;magenta;deep-blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low;Very Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("returns custom positions of labels when label is placed between semicolons.", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;;;Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "" },
{ value: "3", color: "yellow", label: "" },
{ value: "4", color: "blue", label: "Low" }]);
}); //
it("returns custom positions of colors when no color is placed between semicolons.", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("Returns one option when one value,one label, and one are color provided", () => {
expect(InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red",
"Values": "1",
"Labels": "Critical"
}, ["1"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
]);
});
});

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

@ -0,0 +1,129 @@
import { IOption } from "./IOption"
import { Colors} from "./colors"
export class InputParser {
/**
* Parses and gets a FieldName from a dictionary.
* @param {IDictionaryStringTo<string>} inputs - The dictionary has the structure:
* {
* "FieldName": "Priority",
* "Colors": "red;orange;yellow;blue",
* "Values": "0;1;2;3",
* "Labels": "Critical;High;Medium;Low"
* }
* @return {string} The FieldName
* @throws Will throw an {string} error if a FieldName is not specified in the dictionary.
*/
public static getFieldName(inputs: IDictionaryStringTo<string>): string {
if (inputs["FieldName"]) {
return inputs["FieldName"];
}
throw ("FieldName not specified.")
}
/**
* Parses the inputs from a {IDictionaryStringTo<string>} dictionary.
* @return an array of Interfaces of the structure: {
* value: values[i],
* color: colors[i],
* label: labels[i]
* }
* @throws Will throw an {string} error if allowedValues are not specified.
* @throws Will throw an {string} error if Not enough colors provided in admin XML file.
*/
public static getOptions(inputs: IDictionaryStringTo<string>, allowedValues: string[]): IOption[] {
if (allowedValues && allowedValues.length) {
let colors: string[] = [];
let inputColors: string[] = [];
let labels: string[] = [];
let inputLabels: string[] = [];
inputColors = InputParser._extractInputs(inputs["Colors"]);
inputLabels = InputParser._extractInputs(inputs["Labels"]);
colors = InputParser._getColors(inputColors, allowedValues);
labels = InputParser._getLabels(inputLabels, allowedValues);
return InputParser._buildOptions(allowedValues, colors, labels);
} else {
throw ("Allowed values not specified.");
}
}
/**
* Parses {string} rawInput, converting the input to an array of values.
* @param {string} rawInput - The string consists of colors or labels
* separated by ";"
* @return {string[]} inputs (either colors or labels)
* @static
* @private
*/
private static _extractInputs(rawInput: string): string[] {
if (rawInput) {
return rawInput.split(";");
}
return [];
}
/**
* Takes {string[]} inputColors and string{[]} values, and maps {string} colors
* to every value. Also, it checks if the colors were correctly inputed.
* @return {string[]} newColors - An array of {string} colors that match
* the number of values.
* @throws {string} "Not enough colors provided in admin XML file."
* @static
* @private
*/
private static _getColors(inputColors: string[], values: string[]): string[] {
// Values length can never be 0, colors length can be 0 or more
if (values.length > inputColors.length && inputColors.length !== 0) {
// If values array length is greater, an error will appear
throw ("Not enough colors provided in admin XML file.");
}
if (inputColors.length === 0) {
//DefaultColors is a static class wich does the processing of colors.
return Colors.getColors(values.length);
} else {
return values.map((v, idx) => inputColors[idx] || "");
}
}
/**
* Takes {string[]} inputLabels and string{[]} values, and maps {string} labels
* to every value. If more values were provided, it ignores them. If less labels
* than values were provided, it fills the array with empty strings ("");
* @return {string[]} newLabels - An array of {string} labels that match
* the number of values.
* @static
* @private
*/
private static _getLabels(inputLabels: string[], values: string[]): string[] {
// Values length can never be 0, labels length can be 0 or more
// There will be no default labels, just whitespace ""
return values.map((v, idx) => inputLabels[idx] || "");
}
/**
* Takes {string[]} values, colors and labels; and populates an array of interfaces of the
* form {value: "string", color: "string", label: "string"}
* @return {IOptions []} options
* @static
* @private
*/
private static _buildOptions(values: string[], colors: string[], labels: string[]): IOption[] {
let options: IOption[] = [];
let valuesLength: number = values.length;
for (let i = 0; i < valuesLength; i++) {
options.push({
value: values[i],
color: colors[i],
label: labels[i]
});
}
return options;
}
}

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.color-control-dev.color-control-contribution", provider);

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

@ -0,0 +1,54 @@
import { expect } from 'chai';
import { Colors } from './colors';
describe("Colors", () => {
const defaultColors = [
["red"],
["red", "blue"],
["red", "yellow", "blue"],
["red", "orange", "yellow", "blue"],
["red", "orange", "yellow", "blue", "darkblue"],
["darkred", "red", "orange", "yellow", "blue", "darkblue"],
["darkred", "red", "orange", "yellow", "blue", "darkblue", "purple"]
];
// Tests for one value, minimum
it("outputs color array for 1 value", () => {
expect(Colors.getColors(1)).to.be.deep.equal((defaultColors[0]));
});
// Tests for three values, happy path
it("outputs color array for 3 values", () => {
expect(Colors.getColors(3)).to.be.deep.equal((defaultColors[2]));
});
// Tests for seven values, maximum
it("outputs color array for 7 values", () => {
expect(Colors.getColors(7)).to.be.deep.equal((defaultColors[6]));
});
// Tests for eight values, exceeds maximum and should repeat last one
it("outputs color array for 8 values", () => {
expect(Colors.getColors(8)).to.be.deep.equal((["dark red", "red", "orange", "yellow", "blue", "dark blue", "purple", "purple"]));
});
// Tests for twenty values, extreme case, exceeds maximum as well
it("outputs color array for 15 values", () => {
expect(Colors.getColors(15)).to.be.deep.equal((["dark red", "red", "orange", "yellow", "blue", "dark blue", "purple", "purple", "purple", "purple", "purple", "purple", "purple", "purple", "purple"]));
});
// Tests for invalid input of negative
it("throws exception for invalid input of negative", () => {
expect(() => Colors.getColors(-1)).throws(("Incorrect input and no default colors can be provided"));
});
// Tests for invalid input of 0 (no input values)
it("throws exception for invalid input of 0", () => {
expect(() => Colors.getColors(0)).throws(("Incorrect input and no default colors can be provided"));
});
// Tests for invalid input of null
it("throws exception for invalid input of null/undefined", () => {
expect(() => Colors.getColors(null)).throws(("Incorrect input and no default colors can be provided"));
});
});

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

@ -0,0 +1,46 @@
export class Colors {
/** Colors holds a static method called getColors. This method allows InputParser to
* retrieve default colors when the user inputs no colors, but has at least 1 value.
*/
public static getColors(numberOfValues: number): string[] {
/** Takes in the number of values available in the control and returns an array of
* default colors equal to the number of values.
*/
// newColors stores array of default colors for method to return
var newColors: string[] = [];
// defaultColors is an array of default color arrays, allows retrieval of array by index
// Note: Colors need to be changed to official colors, these are just test colors.
const defaultColors = [
["red"],
["red", "blue"],
["red", "yellow", "blue"],
["red", "orange", "yellow", "blue"],
["red", "orange", "yellow", "blue", "darkblue"],
["darkred", "red", "orange", "yellow", "blue", "darkblue"],
["darkred", "red", "orange", "yellow", "blue", "darkblue", "purple"]
];
// Check number of values from input
if (numberOfValues > 0 && numberOfValues <= defaultColors.length) {
// Supports between 1 to 7 values for default colors
newColors = defaultColors[numberOfValues - 1];
return newColors;
}
else if (numberOfValues > defaultColors.length) {
// Does not support beyond the number of default colors, so last color is repeated until all values have an assigned color
newColors = defaultColors[defaultColors.length-1];
for (var i = defaultColors.length; i < numberOfValues; i++) {
newColors.push(defaultColors[defaultColors.length-1][defaultColors.length-1]);
}
return newColors;
}
else {
// Covers null, negative and undefined numberOfValues
throw "Incorrect input and no default colors can be provided";
}
}
}

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

@ -0,0 +1,3 @@
/// <reference path="../typings/index.d.ts" />
import { expect } from 'chai';

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

@ -0,0 +1,87 @@
/** 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 { InputParser } from "./InputParser";
import { Model } from "./model";
import { colorControl } 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: colorControl;
constructor() {
this._initialize();
}
private _initialize(): void {
this._inputs = VSS.getConfiguration().witInputs;
this._fieldName = InputParser.getFieldName(this._inputs);
WitService.WorkItemFormService.getService().then(
(service) => {
Q.spread<any, any>(
[service.getAllowedFieldValues(this._fieldName), service.getFieldValue(this._fieldName)],
(allowedValues: string[], currentValue: (string | number)) => {
if (typeof (currentValue) === 'number') {
allowedValues = allowedValues.sort((a, b) => Number(a) - Number(b));
}
let options = InputParser.getOptions(this._inputs, allowedValues);
this._model = new Model(options, String(currentValue));
this._view = new colorControl(this._model, (val) => {
//when value changes by clicking rows
this._updateInternal(val);
}, () => {//when down or right arrow is used
this._model.selectNextOption();
this._updateInternal(this._model.getSelectedValue());
}, () => {//when up or left arror is used
this._model.selectPreviousOption();
this._updateInternal(this._model.getSelectedValue());
});
}, this._handleError
).then(null, this._handleError);
},
this._handleError);
}
private _handleError(error: string): void {
let errorView = new ErrorView(error);
}
private _updateInternal(value: string): void {
WitService.WorkItemFormService.getService().then(
(service) => {
service.setFieldValue(this._fieldName, value).then(
() => {
this._update(value, true);
}, this._handleError)
},
this._handleError
);
}
private _update(value: string, focus: boolean): void {
this._model.setSelectedValue(value);
this._view.update(value, focus);
}
public updateExternal(value: string): void {
this._update(String(value), false);
}
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);
}
}

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

@ -0,0 +1,96 @@
import { expect } from 'chai';
import { Model } from './model';
import {IOption} from './IOption';
describe("Model", () => {
let model: Model;
const options: IOption[] = [
{
value: "1",
color: "Red",
label: "High"
},
{
value: "2",
color: "Blue",
label: "Medium"
},
{
value: "3",
color: "Green",
label: "Low"
}
];
const testOption: IOption = {
value: "4",
color: "Purple",
label: "Very Low"
};
beforeEach(() => {
model = new Model(options, options[0].value);
});
// Tests for if initial value matches selected value and if it is stored
it("outputs selected value for 1st option", () => {
expect(model.getSelectedValue()).to.be.deep.equal(options[0].value);
});
// Tests for a selected value that is set and one of the values
it("outputs selected value for 2nd option", () => {
model.setSelectedValue(options[1].value);
expect(model.getSelectedValue()).to.be.deep.equal(options[1].value);
});
// Tests for a selected option that is set and one of the options
it("outputs selected option for 2nd option", () => {
model.setSelectedValue(options[1].value);
expect(model.getSelectedOption()).to.be.deep.equal(options[1]);
});
// Tests for a selected value that is null
it("Sets selectedValue to null when no value is selected", () => {
model.setSelectedValue("99");
expect(model.getSelectedValue()).to.be.deep.equal(null);
});
// Tests for a selected value that is null
it("Sets selectedValue to null when no value is selected", () => {
model.setSelectedValue("");
expect(model.getSelectedValue()).to.be.deep.equal(null);
});
// Tests for a selected value that is undefined
it("throws exception for selected value that is undefined", () => {
expect(() => model.setSelectedValue(undefined)).throws("Undefined value");
});
// Tests for the previous option of the first selected option, edge case
it("outputs previous option for 1st selected option: gives first option", () => {
model.setSelectedValue(options[0].value);
model.selectPreviousOption();
expect(model.getSelectedOption()).to.be.deep.equal(options[2]);
});
// Tests for the next option of the last selected option, edge case
it("outputs previous option for last selected option: gives last option", () => {
model.setSelectedValue(options[2].value);
model.selectNextOption();
expect(model.getSelectedOption()).to.be.deep.equal(options[0]);
});
// Tests for the previous option of a selected option
it("outputs previous option for 2nd selected option", () => {
model.setSelectedValue(options[1].value);
model.selectPreviousOption();
expect(model.getSelectedOption()).to.be.deep.equal(options[0]);
});
// Tests for the next option of a selected option
it("outputs next option for 2nd selected option", () => {
model.setSelectedValue(options[1].value);
model.selectNextOption();
expect(model.getSelectedOption()).to.be.deep.equal(options[2]);
});
});

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

@ -0,0 +1,79 @@
import { IOption } from "./IOption";
export class Model {
/**
* Model takes inputs of Options, an array of option objects from InputParser for each
* value, along with its respective color and label, if present. Model also takes the
* initialValue from View, which will be set as the selectedView to begin with.
* This will change as click events occur within View.
*/
constructor(options: IOption[], initialValue: string) {
this._options = options;
this._selectedValue = initialValue;
this._selectedOption = { value: "", color: "", label: "" };
}
// Value selected in View
private _selectedValue: string;
// Option selected in View
private _selectedOption: IOption;
// Array of objects from InputParser (originates from VSS API)
private _options: IOption[] = [];
// Checks if the selected value exists in the array of objects from InputParser.
public setSelectedValue(value: string) {
if(value === undefined){
throw "Undefined value";
}
for (let option of this._options) {
if (option.value === value) {
this._selectedValue = value;
this._selectedOption = option;
return;
}
}
this._selectedValue = null;
this._selectedOption = { value: null, color: "", label: "" };
}
public selectPreviousOption() {
let index = this._options.indexOf(this._selectedOption);
if (index > 0 && index !== -1) {
this.setSelectedValue(this._options[index - 1].value);
} else {
this.setSelectedValue(this._options[this._options.length - 1].value);
}
}
public selectNextOption() {
let index = this._options.indexOf(this._selectedOption);
if (index < (this._options.length - 1) && index !== -1) {
this.setSelectedValue(this._options[index + 1].value);
} else {
this.setSelectedValue(this._options[0].value);
}
}
// Returns the stored selected value to compare with field value from VSS API.
public getSelectedValue(): string {
return this._selectedValue;
}
// Returns the stored selected option for View to update UI.
public getSelectedOption(): IOption {
return this._selectedOption;
}
// Returns the stored array of Options for View to update the UI.
public getOptions(): IOption[] {
return this._options;
}
}

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

@ -0,0 +1,156 @@
/// <reference path="../typings/index.d.ts" />
import { IOption } from "./IOption";
import { Model } from "./model";
/**
* Class colorRow returns the view of a single value, given the parameters allowedValue, color,
* label, and whether or not it's selected.
*/
export class colorRow {
private _row: JQuery;
constructor(public allowedValue: string, public color: string, public label: string) {
}
// creates the row
public create(): JQuery {
// row div
this._row = $("<div> </div>").attr("role", "radio");
this._row.data("value", this.allowedValue);
this._row.addClass("row");
// color div
var valueColor = $("<div> </div>");
valueColor.addClass("valueColor");
var color = this.color;
valueColor.css("background-color", this.color)
this._row.append(valueColor);
// label div
var valueLabel = $("<div> </div>");
valueLabel.addClass("valueLabel");
valueLabel.attr("title", this.label);
if (!this.label) {
valueLabel.text(this.allowedValue);
}
else {
valueLabel.text(this.allowedValue + " - " + this.label);
};
this._row.append(valueLabel);
// return the entire row to the control
return this._row;
}
public select(focus: boolean): void {
this._row.addClass("selected");
this._row.attr("aria-checked", "true");
this._row.attr("tabindex", 0);
if (focus) {
this._row.focus();
}
}
public unselect(): void {
this._row.removeClass("selected");
this._row.attr("aria-checked", "false");
this._row.attr("tabindex", -1);
}
}
/**
* Class colorControl returns a container that renders each row, the selected value,
* and a function that allows the user to change the selected value.
*/
export class colorControl {
public rows: colorRow[] = [];
constructor(private model: Model, private onItemClicked: Function, private onNextItem: Function, private onPreviousItem: Function) {
this.init();
}
// creates the container
public init(): void {
var container = $("<div role='radiogroup'> </div>");
container.addClass("container");
container.attr('tabindex', '0');
var options = this.model.getOptions();
for (let option of options) {
var row = new colorRow(option.value, option.color, option.label);
this.rows.push(row);
container.append(row.create());
// checks if the row is selected and displays accordingly
if (option.value === this.model.getSelectedValue()) {
row.select(true);
}
else {
row.unselect();
}
}
// allows user to click, keyup, or keydown to change the selected value.
$(document).click((evt: JQueryMouseEventObject) => {
this._click(evt);
}).bind('keydown', (evt: JQueryKeyEventObject) => {
if (evt.keyCode == 40 || evt.keyCode == 39) {
// According to ARIA accessibility guide, both down and right arrows should be used.
if (this.onNextItem) {
this.onNextItem();
evt.preventDefault();
}
}
else if (evt.keyCode == 38 || evt.keyCode == 37) {
// According to ARIA accessibility guide, both up and left arrows should be used.
if (this.onPreviousItem) {
this.onPreviousItem();
evt.preventDefault();
}
}
});
$('body').empty().append(container);
$(document).ready(() => {
this._scroll();
});
}
public update(value: string, focus: boolean): void {
for (let row of this.rows) {
if (row.allowedValue == value) {
row.select(focus);
}
else {
row.unselect();
}
}
this._scroll();
}
private _scroll(): void {
let scrollTo = $("div.row.selected");
if (scrollTo.length) {
if (scrollTo.offset().top > $(".container").height()) {
$(".container").scrollTop(
scrollTo.offset().top - $(".container").offset().top + $(".container").scrollTop()
);
}
}
}
private _click(evt: JQueryMouseEventObject): void {
let itemClicked = $(evt.target).closest(".row").data("value");
if (!!itemClicked && !!this.onItemClicked) {
this.onItemClicked(itemClicked);
}
}
}

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

@ -0,0 +1,78 @@
div {
font-family: Segoe UI, Helvetica;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.container:focus {
outline: solid lightgray 1px;
}
.row.selected:focus {
background-color: rgb(220, 230, 244);
}
.row:not(.selected):hover{
background-color:#E3F2FD
}
.container {
height:100%;
border: 1px solid transparent;
}
.container:hover {
overflow-y: auto;
}
.container:not(:hover):not(:focus) .row:not(.selected) > .valueColor {
background-color: gray !important;
}
.container:not(:hover) .row.selected > .valueColor {
color: black;
}
.row {
line-height: 18px;
clear: both;
padding: 3px 2px;
cursor: pointer;
padding: 2px;
outline: none;
}
.row:hover {
background-color: rgb(220, 230, 244);
}
.valueColor {
float: left;
width: 15px;
height: 15px;
border-radius: 25%;
position: relative;
margin-right: 4px;
margin-top: 1px;
opacity: .45;
transition: background linear .2s;
border: 1px solid transparent;
padding-top: 2px;
}
.valueLabel {
color: gray;
}
.row:hover > .valueLabel,
.row.selected > .valueLabel {
color: black;
}
.row:hover > .valueColor,
.row.selected > .valueColor {
opacity: 1;
}

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,0 +1,35 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var ErrorContainer = (function () {
function ErrorContainer(parser, model) {
this._errors = [];
this._errorsInputParser = [];
this._errorsModel = [];
this._parser = parser;
this._model = model;
}
ErrorContainer.prototype.hasErrors = function () {
this._errorsInputParser = this._parser.getErrors();
this._errorsModel = this._model.getErrors();
if (this._errorsInputParser || this._errorsModel)
return true;
return false;
};
ErrorContainer.prototype.getErrors = function () {
for (var _i = 0, _a = this._errorsInputParser; _i < _a.length; _i++) {
var error = _a[_i];
this._errors.push(error);
}
for (var _b = 0, _c = this._errorsModel; _b < _c.length; _b++) {
var error = _c[_b];
this._errors.push(error);
}
return this._errors;
};
ErrorContainer.prototype.push = function (error) {
this._errors.push(error);
};
return ErrorContainer;
}());
exports.ErrorContainer = ErrorContainer;
});

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

@ -0,0 +1,3 @@
define(["require", "exports"], function (require, exports) {
"use strict";
});

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

@ -0,0 +1,63 @@
define(["require", "exports", "./colors"], function (require, exports, colors_1) {
"use strict";
var InputParser = (function () {
function InputParser() {
}
InputParser.getFieldName = function (inputs) {
if (inputs["FieldName"]) {
return inputs["FieldName"];
}
throw ("FieldName not specified.");
};
InputParser.getOptions = function (inputs, allowedValues) {
if (allowedValues && allowedValues.length) {
var colors = [];
var inputColors = [];
var labels = [];
var inputLabels = [];
inputColors = InputParser.extractInputs(inputs["Colors"]);
inputLabels = InputParser.extractInputs(inputs["Labels"]);
colors = InputParser.getColors(inputColors, allowedValues);
labels = InputParser.getLabels(inputLabels, allowedValues);
return InputParser.buildOptions(allowedValues, colors, labels);
}
else {
throw ("Allowed values not specified.");
}
};
InputParser.extractInputs = function (rawInput) {
if (rawInput) {
return rawInput.split(";");
}
return [];
};
InputParser.getColors = function (inputColors, values) {
if (values.length > inputColors.length && inputColors.length !== 0) {
throw ("Not enough colors provided in admin XML file.");
}
if (inputColors.length === 0) {
return colors_1.Colors.getColors(values.length);
}
else {
return values.map(function (v, idx) { return inputColors[idx] || ""; });
}
};
InputParser.getLabels = function (inputLabels, values) {
return values.map(function (v, idx) { return inputLabels[idx] || ""; });
};
InputParser.buildOptions = function (values, colors, labels) {
var options = [];
var valuesLength = values.length;
for (var i = 0; i < valuesLength; i++) {
options.push({
value: values[i],
color: colors[i],
label: labels[i]
});
}
return options;
};
return InputParser;
}());
exports.InputParser = InputParser;
});

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

@ -0,0 +1,60 @@
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
define(["require", "exports", 'chai', "./InputParser"], function (require, exports, chai_1, InputParser_1) {
"use strict";
var TestableInputParser = (function (_super) {
__extends(TestableInputParser, _super);
function TestableInputParser() {
_super.apply(this, arguments);
}
TestableInputParser.prototype.testableExtractInputs = function (customInputs) {
var inputs = customInputs;
this._extractFieldName(inputs);
this._values = this._parseInput(inputs["Values"]);
this._colors = this._parseInput(inputs["Colors"]);
this._labels = this._parseInput(inputs["Labels"]);
};
return TestableInputParser;
}(InputParser_1.InputParser));
describe("inputParser", function () {
var inputParser;
beforeEach(function () {
inputParser = new TestableInputParser();
});
inputParser.testableExtractInputs({ "FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "0;1;2;3",
"Labels": "Critical;High;Medium;Low" });
it("outputs number when not divisible by 3 or 5", function () {
chai_1.expect(inputParser.getParsedFieldName()).to.be.equal("Priority");
chai_1.expect(inputParser.getParsedColors()).to.be.equal(["red", "orange", "yellow", "blue"]);
chai_1.expect(inputParser.getParsedValues()).to.be.equal(["0", "1", "2", "3"]);
chai_1.expect(inputParser.getParsedLabels()).to.be.equal(["Critical", "High", "Medium", "Low"]);
chai_1.expect(inputParser.getErrors()).to.be.equal([]);
});
inputParser.testableExtractInputs({ "FieldName": "",
"Colors": "red;orange;yellow;blue",
"Values": "0;1;2;3",
"Labels": "Critical;High;Medium;Low" });
it("outputs number when not divisible by 3 or 5", function () {
chai_1.expect(inputParser.getParsedFieldName()).to.be.equal("Priority");
chai_1.expect(inputParser.getParsedColors()).to.be.equal(["red", "orange", "yellow", "blue"]);
chai_1.expect(inputParser.getParsedValues()).to.be.equal(["0", "1", "2", "3"]);
chai_1.expect(inputParser.getParsedLabels()).to.be.equal(["FieldName not found"]);
});
inputParser.testableExtractInputs({ "FieldName": "",
"Colors": ";;;",
"Values": ";;;",
"Labels": ";;;" });
it("Checks if error with empty string", function () {
chai_1.expect(inputParser.getParsedFieldName()).to.be.equal("");
chai_1.expect(inputParser.getParsedColors()).to.be.equal(["", "", "", ""]);
chai_1.expect(inputParser.getParsedValues()).to.be.equal(["", "", ""]);
chai_1.expect(inputParser.getParsedLabels()).to.be.equal(["", "", "", ""]);
chai_1.expect(inputParser.getParsedLabels()).to.be.equal(["FieldName not found"]);
});
});
});

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

@ -0,0 +1,146 @@
define(["require", "exports", 'chai', "./InputParser"], function (require, exports, chai_1, InputParser_1) {
"use strict";
describe("InputParser", function () {
var bestCaseDict = {
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "0;1;2;3",
"Labels": "Critical;High;Medium;Low"
};
var bestCaseValues = ["1", "2", "3", "4"];
it("gets the field name specified in dictionary", function () {
chai_1.expect(InputParser_1.InputParser.getFieldName(bestCaseDict)).to.be.deep.equal("Priority");
});
it("throws when field name not specified", function () {
chai_1.expect(function () { return InputParser_1.InputParser.getFieldName({
"FieldName": ""
}); }).throw("FieldName not specified.");
});
it("returns an array of interfaces", function () {
chai_1.expect(InputParser_1.InputParser.getOptions(bestCaseDict, bestCaseValues)).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("returns options with empty strings in label key when no labels are provided", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": ""
}, bestCaseValues)).to.be.deep.equal([
{ value: "1", color: "red", label: "" },
{ value: "2", color: "orange", label: "" },
{ value: "3", color: "yellow", label: "" },
{ value: "4", color: "blue", label: "" }]);
});
it("returns 1 default color when 1 value and no colors are provided", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "",
"Values": "1",
"Labels": "Critical"
}, ["1"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" }]);
});
it("returns options with default colors and NO labels when NO colors and NO labels provided.", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "",
"Values": "1;2;3;4",
"Labels": ""
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "" },
{ value: "2", color: "orange", label: "" },
{ value: "3", color: "yellow", label: "" },
{ value: "4", color: "blue", label: "" }]);
});
it("throws when allowed values are not specified", function () {
chai_1.expect(function () { return InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "",
"Labels": "Critical;High;Medium"
}, []); }).throw("Allowed values not specified.");
});
it("Returns options with some empty labels if less labels than values provided", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "" }]);
});
it("throws when less colors than values are provided", function () {
chai_1.expect(function () { return InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low"
}, ["1", "2", "3", "4"]); }).throw("Not enough colors provided in admin XML file.");
});
it("gives one label to every value, and truncates unused labels when MORE Labels THAN values are provided", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low;Very Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("gives one color to every value, and truncates unused colors when MORE colors THAN values are provided", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue;magenta;deep-blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low;Very Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("returns custom positions of labels when label is placed between semicolons.", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;orange;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;;;Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "orange", label: "" },
{ value: "3", color: "yellow", label: "" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("returns custom positions of colors when no color is placed between semicolons.", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red;;yellow;blue",
"Values": "1;2;3;4",
"Labels": "Critical;High;Medium;Low"
}, ["1", "2", "3", "4"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
{ value: "2", color: "", label: "High" },
{ value: "3", color: "yellow", label: "Medium" },
{ value: "4", color: "blue", label: "Low" }]);
});
it("Returns one option when one value,one label, and one are color provided", function () {
chai_1.expect(InputParser_1.InputParser.getOptions({
"FieldName": "Priority",
"Colors": "red",
"Values": "1",
"Labels": "Critical"
}, ["1"])).to.be.deep.equal([
{ value: "1", color: "red", label: "Critical" },
]);
});
});
});

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

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

@ -0,0 +1,35 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var Colors = (function () {
function Colors() {
}
Colors.getColors = function (numberOfValues) {
var newColors = [];
var defaultColors = [
["red"],
["red", "blue"],
["red", "yellow", "blue"],
["red", "orange", "yellow", "blue"],
["red", "orange", "yellow", "blue", "dark blue"],
["dark red", "red", "orange", "yellow", "blue", "dark blue"],
["dark red", "red", "orange", "yellow", "blue", "dark blue", "purple"]
];
if (numberOfValues > 0 && numberOfValues <= defaultColors.length) {
newColors = defaultColors[numberOfValues - 1];
return newColors;
}
else if (numberOfValues > defaultColors.length) {
newColors = defaultColors[defaultColors.length - 1];
for (var i = defaultColors.length; i < numberOfValues; i++) {
newColors.push(defaultColors[defaultColors.length - 1][defaultColors.length - 1]);
}
return newColors;
}
else {
throw "Incorrect input and no default colors can be provided";
}
};
return Colors;
}());
exports.Colors = Colors;
});

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

@ -0,0 +1,38 @@
define(["require", "exports", 'chai', './colors'], function (require, exports, chai_1, colors_1) {
"use strict";
describe("Colors", function () {
var defaultColors = [
["red"],
["red", "blue"],
["red", "yellow", "blue"],
["red", "orange", "yellow", "blue"],
["red", "orange", "yellow", "blue", "dark blue"],
["dark red", "red", "orange", "yellow", "blue", "dark blue"],
["dark red", "red", "orange", "yellow", "blue", "dark blue", "purple"]
];
it("outputs color array for 1 value", function () {
chai_1.expect(colors_1.Colors.getColors(1)).to.be.deep.equal((defaultColors[0]));
});
it("outputs color array for 3 values", function () {
chai_1.expect(colors_1.Colors.getColors(3)).to.be.deep.equal((defaultColors[2]));
});
it("outputs color array for 7 values", function () {
chai_1.expect(colors_1.Colors.getColors(7)).to.be.deep.equal((defaultColors[6]));
});
it("outputs color array for 8 values", function () {
chai_1.expect(colors_1.Colors.getColors(8)).to.be.deep.equal((["dark red", "red", "orange", "yellow", "blue", "dark blue", "purple", "purple"]));
});
it("outputs color array for 15 values", function () {
chai_1.expect(colors_1.Colors.getColors(15)).to.be.deep.equal((["dark red", "red", "orange", "yellow", "blue", "dark blue", "purple", "purple", "purple", "purple", "purple", "purple", "purple", "purple", "purple"]));
});
it("throws exception for invalid input of negative", function () {
chai_1.expect(function () { return colors_1.Colors.getColors(-1); }).throws(("Incorrect input and no default colors can be provided"));
});
it("throws exception for invalid input of 0", function () {
chai_1.expect(function () { return colors_1.Colors.getColors(0); }).throws(("Incorrect input and no default colors can be provided"));
});
it("throws exception for invalid input of null/undefined", function () {
chai_1.expect(function () { return colors_1.Colors.getColors(null); }).throws(("Incorrect input and no default colors can be provided"));
});
});
});

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

@ -0,0 +1,36 @@
define(["require", "exports", "TFS/WorkItemTracking/Services", "./InputParser", "./model", "./view", "./errorView"], function (require, exports, WitService, InputParser_1, model_1, view_1, errorView_1) {
"use strict";
var Controller = (function () {
function Controller() {
this._fieldName = "";
this._initialize();
}
Controller.prototype._initialize = function () {
var _this = this;
this._inputs = VSS.getConfiguration().witInputs;
this._fieldName = InputParser_1.InputParser.getFieldName(this._inputs);
WitService.WorkItemFormService.getService().then(function (service) {
Q.spread([service.getAllowedFieldValues(_this._fieldName), service.getFieldValue(_this._fieldName)], function (allowedValues, currentValue) {
var options = InputParser_1.InputParser.getOptions(_this._inputs, allowedValues);
_this._model = new model_1.Model(options, currentValue);
_this._view = new view_1.colorControl(_this._model, function (val) {
_this.update(val);
});
}, _this.handleError);
}, this.handleError);
};
Controller.prototype.handleError = function (error) {
var errorView = new errorView_1.ErrorView(error);
};
Controller.prototype.update = function (value) {
var _this = this;
WitService.WorkItemFormService.getService().then(function (service) {
service.setFieldValue(_this._fieldName, value).then(function () {
_this._model.setSelectedValue(value);
_this._view.update(value);
}, _this.handleError);
}, this.handleError);
};
return Controller;
}());
});

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

@ -0,0 +1,3 @@
define(["require", "exports"], function (require, exports) {
"use strict";
});

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

@ -0,0 +1,23 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var ErrorView = (function () {
function ErrorView(error) {
var container = $("<div />");
container.addClass("container");
var warning = $("<p />");
warning.prepend('<span class="bowtie-icon">bowtie-status-warning</span>');
warning.text(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.text("Documentation.");
help.append(a);
container.append(help);
$('body').empty().append(container);
}
return ErrorView;
}());
exports.ErrorView = ErrorView;
});

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

@ -0,0 +1,46 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var Model = (function () {
function Model(options, initialValue) {
this._options = [];
this._options = options;
this._selectedValue = initialValue;
}
Model.prototype.setSelectedValue = function (value) {
for (var _i = 0, _a = this._options; _i < _a.length; _i++) {
var option = _a[_i];
if (option.value === value) {
this._selectedValue = value;
this._selectedOption = option;
break;
}
}
if (this._selectedValue !== value) {
throw "Selected value not within original values";
}
};
Model.prototype.selectPreviousOption = function () {
var index = this._options.indexOf(this._selectedOption);
if (index > 0) {
this.setSelectedValue(this._options[index - 1].value);
}
};
Model.prototype.selectNextOption = function () {
var index = this._options.indexOf(this._selectedOption);
if (index < (this._options.length - 1)) {
this.setSelectedValue(this._options[index + 1].value);
}
};
Model.prototype.getSelectedValue = function () {
return this._selectedValue;
};
Model.prototype.getSelectedOption = function () {
return this._selectedOption;
};
Model.prototype.getOptions = function () {
return this._options;
};
return Model;
}());
exports.Model = Model;
});

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

@ -0,0 +1,71 @@
define(["require", "exports", 'chai', './model'], function (require, exports, chai_1, model_1) {
"use strict";
describe("Model", function () {
var model;
var options = [
{
value: "1",
color: "Red",
label: "High"
},
{
value: "2",
color: "Blue",
label: "Medium"
},
{
value: "3",
color: "Green",
label: "Low"
}
];
var testOption = {
value: "4",
color: "Purple",
label: "Very Low"
};
beforeEach(function () {
model = new model_1.Model(options, options[0].value);
});
it("outputs selected value for 1st option", function () {
chai_1.expect(model.getSelectedValue()).to.be.deep.equal(options[0].value);
});
it("outputs selected value for 2nd option", function () {
model.setSelectedValue(options[1].value);
chai_1.expect(model.getSelectedValue()).to.be.deep.equal(options[1].value);
});
it("outputs selected option for 2nd option", function () {
model.setSelectedValue(options[1].value);
chai_1.expect(model.getSelectedOption()).to.be.deep.equal(options[1]);
});
it("throws exception for selected value that is not one of values", function () {
chai_1.expect(function () { return model.setSelectedValue(testOption.value); }).throws("Selected value not within original values");
});
it("throws exception for selected value that is null", function () {
chai_1.expect(function () { return model.setSelectedValue(null); }).throws("Selected value not within original values");
});
it("throws exception for selected value that is null", function () {
chai_1.expect(function () { return model.setSelectedValue(undefined); }).throws("Selected value not within original values");
});
it("outputs previous option for 1st selected option: gives first option", function () {
model.setSelectedValue(options[0].value);
model.selectPreviousOption();
chai_1.expect(model.getSelectedOption()).to.be.deep.equal(options[0]);
});
it("outputs previous option for last selected option: gives last option", function () {
model.setSelectedValue(options[2].value);
model.selectNextOption();
chai_1.expect(model.getSelectedOption()).to.be.deep.equal(options[2]);
});
it("outputs previous option for 2nd selected option", function () {
model.setSelectedValue(options[1].value);
model.selectPreviousOption();
chai_1.expect(model.getSelectedOption()).to.be.deep.equal(options[0]);
});
it("outputs next option for 2nd selected option", function () {
model.setSelectedValue(options[1].value);
model.selectNextOption();
chai_1.expect(model.getSelectedOption()).to.be.deep.equal(options[2]);
});
});
});

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

@ -0,0 +1,113 @@
define(["require", "exports"], function (require, exports) {
"use strict";
var colorRow = (function () {
function colorRow(allowedValue, color, label) {
this.allowedValue = allowedValue;
this.color = color;
this.label = label;
}
colorRow.prototype.create = function () {
this._row = $("<div> </div>");
this._row.data("value", this.allowedValue);
this._row.addClass("row");
var valueColor = $("<div> </div>");
valueColor.addClass("valueColor");
var color = this.color;
valueColor.css("background-color", this.color);
this._row.append(valueColor);
var valueLabel = $("<div> </div>");
valueLabel.addClass("valueLabel");
if (!this.label) {
valueLabel.text(this.allowedValue);
}
else {
valueLabel.text(this.allowedValue + " - " + this.label);
}
;
this._row.append(valueLabel);
return this._row;
};
colorRow.prototype.select = function () {
this._row.addClass("selected");
};
colorRow.prototype.unselect = function () {
this._row.removeClass("selected");
};
return colorRow;
}());
exports.colorRow = colorRow;
var colorControl = (function () {
function colorControl(model, onItemClicked) {
this.model = model;
this.onItemClicked = onItemClicked;
this.rows = [];
this.init();
}
colorControl.prototype.init = function () {
var _this = this;
var container = $("<div> </div>");
container.addClass("container");
var rowSelected = this.model.getSelectedOption();
var options = this.model.getOptions();
for (var _i = 0, options_1 = options; _i < options_1.length; _i++) {
var option = options_1[_i];
var row = new colorRow(option.value, option.color, option.label);
this.rows.push(row);
container.append(row.create());
var selected = option.value === rowSelected.value;
if (selected) {
row.select();
}
else {
row.unselect();
}
}
var callback = function (evt) {
if (evt.keyCode == 40) {
if (rowSelected) {
_this.model.selectNextOption();
var itemClicked = _this.model.getSelectedOption();
if (_this.onItemClicked) {
_this.onItemClicked(itemClicked.value);
}
}
}
else if (evt.keyCode == 38) {
if (rowSelected) {
_this.model.selectPreviousOption();
var next = _this.model.getSelectedOption();
var itemClicked = next.value;
if (_this.onItemClicked) {
_this.onItemClicked(itemClicked.value);
}
}
}
};
container.click(function (evt) {
var itemClicked = $(evt.target).closest(".row").data("value");
if (_this.onItemClicked) {
_this.onItemClicked(itemClicked);
}
});
container.keydown(function (evt) {
callback(evt);
}).keyup(function (evt) {
callback(evt);
});
$('body').empty().append(container);
};
colorControl.prototype.update = function (value) {
for (var _i = 0, _a = this.rows; _i < _a.length; _i++) {
var row = _a[_i];
if (row.allowedValue == value) {
row.select();
}
else {
row.unselect();
}
}
};
return colorControl;
}());
exports.colorControl = colorControl;
});

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

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

@ -0,0 +1,114 @@
{
"manifestVersion": 1,
"id": "color-control",
"version": "0.1.0",
"name": "Color Form Control",
"scopes": [
"vso.work",
"vso.work_write"
],
"description": "Describe your extension.",
"publisher": "mariamclaughlin",
"icons": {
"default": "img/logo.JPG"
},
"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": "color-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": 90,
"inputs": [
{
"id": "FieldName",
"description": "The field associated with the control.",
"validation": {
"dataType": "Field",
"isRequired": true
}
},
{
"id": "Labels",
"description": "The list of values to select from.",
"validation": {
"dataType": "String",
"isRequired": false
}
},
{
"id": "Colors",
"description": "The field associated with the control.",
"validation": {
"dataType": "String",
"isRequired": false
}
}
]
}
}
]
}