initial commit
This commit is contained in:
Коммит
b3f888b492
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
scripts/*.js
|
||||
typings
|
||||
dist
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "color-control-dev",
|
||||
"name": "Color Form Control (dev)",
|
||||
"public": false
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"public": true
|
||||
}
|
|
@ -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*.
|
|
@ -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"]);
|
||||
};
|
|
@ -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>
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 28 KiB |
|
@ -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>
|
|
@ -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
|
||||
})
|
||||
}
|
Двоичный файл не отображается.
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
```
|
|
@ -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" },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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"));
|
||||
});
|
||||
});
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
/// <reference path="../typings/index.d.ts" />
|
||||
|
||||
import { expect } from 'chai';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
});
|
|
@ -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,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"));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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";
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "amd",
|
||||
"sourceMap": false
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Загрузка…
Ссылка в новой задаче