Initial changes
|
@ -57,3 +57,7 @@ typings/
|
|||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# Specific to this repo
|
||||
*.vsix
|
||||
*.docx
|
||||
dist
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Jest All",
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
||||
"args": ["--runInBand"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Jest Current File",
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
||||
"args": ["${relativeFile}"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
66
README.md
|
@ -1,14 +1,52 @@
|
|||
|
||||
# Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
**Plan or track work items in progress by visualizing them on a sprint calendar**.
|
||||
Portfolio level work items are worked for multiple sprints and this tool helps you visualize features or epics across sprints, yes you heard it right, **cross sprint schedule**!!!
|
||||
|
||||
![Feature timeline](images/main-img.png "Feature timeline")
|
||||
|
||||
|
||||
## Get started
|
||||
Building and testing the extension requires following.
|
||||
|
||||
1) [Download and install nodejs](http://nodejs.org "nodejs")
|
||||
2) [webpack](https://webpack.js.org/)
|
||||
3) [tfx cli](https://docs.microsoft.com/en-us/vsts/extend/publish/command-line?view=vsts)
|
||||
4) [TypeScript](https://www.typescriptlang.org/)
|
||||
```
|
||||
npm i -g typescript tfs-cli webpack
|
||||
```
|
||||
|
||||
Install dev prerequisites
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Create vsix to deploy on test environment
|
||||
```
|
||||
webpack && npm run package:dev:http
|
||||
```
|
||||
### Run the extension server locally
|
||||
Execute following commands in two separate Command Prompts
|
||||
```
|
||||
webpack --watch
|
||||
npm run dev:http
|
||||
```
|
||||
### Publish the dev extension to marketplace
|
||||
Follow the instructions here
|
||||
|
||||
[Package, publish, unpublish, and install VSTS extensions
|
||||
](https://docs.microsoft.com/en-us/vsts/extend/publish/overview?view=vsts)
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Feature Timeline (BETA)",
|
||||
"galleryFlags": [
|
||||
"Preview"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"public": false
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"public": false,
|
||||
"baseUri": "http://localhost:8888"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"public": true
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
**Plan or track work items in progress by visualizing them on a sprint calendar**.
|
||||
Portfolio level work items are worked for multiple sprints and this tool helps you visualize features or epics across sprints, yes you heard it right, **cross sprint schedule**!!!
|
||||
|
||||
![Feature timeline](dist/images/main-img.png "Feature timeline")
|
||||
|
||||
# Overview
|
||||
|
||||
We see two kinds of teams:
|
||||
|
||||
Scrum Teams: They commit to User Stories for each sprint and use sprint tools (Sprint backlog, Taskboard) to execute and track their work for each sprint.
|
||||
|
||||
Kanban Teams: They pull highest priority work from backlog and execute work to completion before picking the next one. Usually these teams do not set specific sprint/iteration for each work they are executing on.
|
||||
|
||||
While both Kanban teams and Scrum Teams have User Stories or Tasks that are smaller chunk of work that is finished within a sprint, they do rollup to a portfolio level work like **feature that span across sprint.**
|
||||
|
||||
# How does it work?
|
||||
|
||||
If you are a scrum team that sets iteration on each user story then you will be incentivized, because we rollup child user story iterations to its parent feature, yes you are rewarded for planning your child user stories.
|
||||
|
||||
![Child rollup](dist/images/child-rollup.png "Child rollup")
|
||||
|
||||
If you are a kanban team and does not set iterations for your user story, plan when your features will tentatively start/finish by manually extending your features sprints.
|
||||
|
||||
![Manual planning](dist/images/manual-plan1.gif "Manual Planning")
|
||||
|
||||
If we do not have both these data points, we put the feature on the feature iteration if available else it goes to current sprint so that you get a chance to set it.
|
||||
|
||||
You can also drag drop the work items to plan in the tool.
|
||||
|
||||
# Get Started
|
||||
|
||||
This extension is a pivot available for portfolio level backlogs. Not to pollute your timeline we only pull work items that are in " **InProgress**" state category.
|
||||
|
||||
Give it a try. Looking forward to hearing your feedback.
|
После Ширина: | Высота: | Размер: 38 KiB |
После Ширина: | Высота: | Размер: 22 KiB |
После Ширина: | Высота: | Размер: 256 KiB |
После Ширина: | Высота: | Размер: 38 KiB |
После Ширина: | Высота: | Размер: 23 KiB |
После Ширина: | Высота: | Размер: 22 KiB |
После Ширина: | Высота: | Размер: 256 KiB |
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"name": "feature-timeline",
|
||||
"version": "1.0.0",
|
||||
"author": "ms-devlabs",
|
||||
"license": "MIT",
|
||||
"description": "A Work Item view.",
|
||||
"main": "webpack.config.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist *.vsix vss-extension-release.json src/*js libs",
|
||||
"dev": "webpack-dev-server --hot --progress --colors --content-base ./dist --https --port 8888",
|
||||
"dev:http": "webpack-dev-server --progress --colors --content-base ./ --port 8888",
|
||||
"package:dev": "node ./scripts/packageDev",
|
||||
"package:dev:http": "node ./scripts/packageDevHttp",
|
||||
"package:release": "node ./scripts/packageRelease",
|
||||
"package:beta": "node ./scripts/packageBeta",
|
||||
"publish:dev": "npm run package:dev && node ./scripts/publishDev",
|
||||
"build:release": "set NODE_ENV=production && npm run clean && mkdir dist && webpack --progress --colors --output-path ./dist -p && set NODE_ENV=",
|
||||
"publish:release": "npm run build:release && node ./scripts/publishRelease",
|
||||
"test": "jest",
|
||||
"testupdate": "jest --updateSnapshot",
|
||||
"postinstall": "typings install"
|
||||
},
|
||||
"keywords": [
|
||||
"timeline",
|
||||
"work item"
|
||||
],
|
||||
"dependencies": {
|
||||
"office-ui-fabric-react": "^5.68.0",
|
||||
"react": "^16.2.0",
|
||||
"react-dnd": "^2.6.0",
|
||||
"react-dnd-html5-backend": "^2.6.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"redux": "^3.7.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-saga": "^0.16.0",
|
||||
"reselect": "^3.0.1",
|
||||
"vss-web-extension-sdk": "^5.131.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^22.2.2",
|
||||
"@types/jquery": "^2.0.41",
|
||||
"@types/react": "^15.0.21",
|
||||
"@types/react-dom": "^0.14.23",
|
||||
"@types/react-redux": "^5.0.15",
|
||||
"awesome-typescript-loader": "^4.0.1",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"jest": "^22.4.3",
|
||||
"node-sass": "^4.8.3",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"sass-loader": "^6.0.7",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"style-loader": "^0.16.1",
|
||||
"tfx-cli": "^0.4.5",
|
||||
"ts-jest": "^22.4.2",
|
||||
"ts-loader": "^4.0.1",
|
||||
"typescript": "^2.7.2",
|
||||
"typings": "^2.1.0",
|
||||
"uglifyjs-webpack-plugin": "^0.4.2",
|
||||
"webpack": "^4.2.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-cli": "^2.0.13",
|
||||
"webpack-dev-server": "^3.1.1"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
"node_modules/vss-web-extension-sdk/lib"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^VSS(.*)$": "<rootDir>/node_modules/vss-web-extension-sdk/lib/VSS.SDK.min.js",
|
||||
"^TFS(.*)$": "<rootDir>/node_modules/vss-web-extension-sdk/lib/VSS.SDK.min.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
"use strict";
|
||||
|
||||
var exec = require("child_process").exec;
|
||||
|
||||
var manifest = require("../vss-extension.json");
|
||||
var extensionId = manifest.id;
|
||||
|
||||
// Package extension
|
||||
var command = `tfx extension create --extension-id ${extensionId}-beta --overrides-file configs/beta.json --manifest-globs vss-extension.json --no-prompt --json`;
|
||||
exec(command, (error, stdout) => {
|
||||
if (error) {
|
||||
console.error(`Could not create package: '${error}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = JSON.parse(stdout);
|
||||
|
||||
console.log(`Package created ${output.path}`);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,15 @@
|
|||
var exec = require("child_process").exec;
|
||||
|
||||
// Load existing publisher
|
||||
var manifest = require("../vss-extension.json");
|
||||
var extensionId = manifest.id;
|
||||
|
||||
// Package extension
|
||||
var command = `tfx extension create --rev-version --overrides-file configs/dev.json --manifest-globs vss-extension.json --extension-id ${extensionId}-dev --no-prompt`;
|
||||
exec(command, function (error) {
|
||||
if (error) {
|
||||
console.log(`Package create error: ${error}`);
|
||||
} else {
|
||||
console.log("Package created");
|
||||
}
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
var exec = require("child_process").exec;
|
||||
|
||||
// Load existing publisher
|
||||
var manifest = require("../vss-extension.json");
|
||||
var extensionId = manifest.id;
|
||||
|
||||
// Package extension
|
||||
var command = `tfx extension create --rev-version --overrides-file configs/devHttp.json --manifest-globs vss-extension.json --extension-id ${extensionId}-dev --no-prompt`;
|
||||
exec(command, function() {
|
||||
console.log("Package created");
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
var exec = require("child_process").exec;
|
||||
|
||||
// Package extension
|
||||
var command = `tfx extension create --rev-version --overrides-file configs/release.json --manifest-globs vss-extension.json --no-prompt --json`;
|
||||
exec(command, (error, stdout) => {
|
||||
if (error) {
|
||||
console.error(`Could not create package: '${error}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = JSON.parse(stdout);
|
||||
|
||||
console.log(`Package created ${output.path}`);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,16 @@
|
|||
var exec = require("child_process").exec;
|
||||
|
||||
var manifest = require("../vss-extension.json");
|
||||
var extensionId = manifest.id;
|
||||
var extensionPublisher = manifest.publisher;
|
||||
var extensionVersion = manifest.version;
|
||||
|
||||
// Package extension
|
||||
var command = `tfx extension publish --vsix ${extensionPublisher}.${extensionId}-dev-${extensionVersion}.vsix --save`;
|
||||
exec(command, function (error) {
|
||||
if (error) {
|
||||
console.log("Package publish ERROR:" + error);
|
||||
} else {
|
||||
console.log("Package published.");
|
||||
}
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
"use strict";
|
||||
|
||||
var exec = require("child_process").exec;
|
||||
|
||||
// Package extension
|
||||
var command = `tfx extension create --rev-version --overrides-file ../configs/release.json --manifest-globs vss-extension-release.json --no-prompt --json`;
|
||||
exec(command, {
|
||||
"cwd": "./dist"
|
||||
}, (error, stdout) => {
|
||||
if (error) {
|
||||
console.error(`Could not create package: '${error}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = JSON.parse(stdout);
|
||||
|
||||
console.log(`Package created ${output.path}`);
|
||||
|
||||
var command = `tfx extension publish --vsix ${output.path} --no-prompt`;
|
||||
exec(command, (error, stdout) => {
|
||||
if (error) {
|
||||
console.error(`Could not create package: '${error}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Package published.");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { DragDropGrid } from "./react/Components/FeatureTimelineGrid";
|
||||
import { iePollyfill } from "./polyfill";
|
||||
|
||||
export function initialize(): void {
|
||||
if (!isBackground()) {
|
||||
iePollyfill();
|
||||
ReactDOM.render(
|
||||
<DragDropGrid /> , document.getElementById("root"));
|
||||
}
|
||||
}
|
||||
|
||||
export function unmount(): void {
|
||||
if (!isBackground()) {
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
|
||||
}
|
||||
}
|
||||
|
||||
function isBackground() {
|
||||
const contributionContext = VSS.getConfiguration();
|
||||
return contributionContext.host && contributionContext.host.background;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { WorkItemType, WorkItemStateColor } from "TFS/WorkItemTracking/Contracts";
|
||||
import { getClient } from "VSS/Service";
|
||||
import { WorkItemTrackingHttpClient } from "TFS/WorkItemTracking/RestClient";
|
||||
|
||||
export class WorkItemMetadataService {
|
||||
private static _instance: WorkItemMetadataService;
|
||||
public static getInstance(): WorkItemMetadataService {
|
||||
if (!WorkItemMetadataService._instance) {
|
||||
|
||||
WorkItemMetadataService._instance = new WorkItemMetadataService();
|
||||
}
|
||||
return WorkItemMetadataService._instance;
|
||||
}
|
||||
|
||||
private _workItemTypes: WorkItemType[] = null;
|
||||
|
||||
public async getWorkItemTypes(projectId): Promise<WorkItemType[]> {
|
||||
if (this._workItemTypes) {
|
||||
return this._workItemTypes;
|
||||
}
|
||||
|
||||
const witHttpClient = getClient(WorkItemTrackingHttpClient);
|
||||
this._workItemTypes = await witHttpClient.getWorkItemTypes(projectId);
|
||||
|
||||
return this._workItemTypes;
|
||||
}
|
||||
|
||||
|
||||
private _states: IDictionaryStringTo<WorkItemStateColor[]> = null;
|
||||
|
||||
public async getStates(projectId): Promise<IDictionaryStringTo<WorkItemStateColor[]>> {
|
||||
if (this._states) {
|
||||
return this._states;
|
||||
}
|
||||
|
||||
const witHttpClient = getClient(WorkItemTrackingHttpClient);
|
||||
const workItemTypes = await this.getWorkItemTypes(projectId);
|
||||
|
||||
const map = {};
|
||||
for (const wit of workItemTypes) {
|
||||
map[wit.name] = await witHttpClient.getWorkItemTypeStates(projectId, wit.referenceName);
|
||||
}
|
||||
|
||||
this._states = map;
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" style="height:100%">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- VSS Framework -->
|
||||
<script src="libs/VSS.SDK.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body style="height:100%">
|
||||
<script>
|
||||
let FeatureTimeline = null;
|
||||
VSS.init({
|
||||
usePlatformStyles: true,
|
||||
explicitNotifyLoaded: true,
|
||||
usePlatformScripts: true,
|
||||
extensionReusedCallback: registerContribution,
|
||||
moduleLoaderConfig: {
|
||||
paths: {
|
||||
"react": "dist/react",
|
||||
"react-dom": "dist/react-dom",
|
||||
"FeatureTimeline": "dist/bundle"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We need to register the new contribution if this extension host is reused
|
||||
function registerContribution(contribution) {
|
||||
if (contribution.type === "ms.vss-web.tab") {
|
||||
// Register the fully-qualified contribution id here.
|
||||
// Because we're using the contribution id, we do NOT need to define a registeredObjectId in the extension manfiest.
|
||||
VSS.register(contribution.id, {
|
||||
pageTitle: "Feature Timeline",
|
||||
// We set the "dynamic" contribution property to true in the manifest so that it will get the tab name from this function.
|
||||
name: "Feature Timeline",
|
||||
title: "Feature Timeline",
|
||||
updateContext: updateConfiguration,
|
||||
isInvisible: function (state) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let previousContext = null
|
||||
|
||||
function updateConfiguration(tabContext) {
|
||||
if (!isBackground() && typeof tabContext === "object" && FeatureTimeline && JSON.stringify(previousContext) !== JSON.stringify(tabContext)) {
|
||||
FeatureTimeline.unmount();
|
||||
FeatureTimeline.initialize();
|
||||
previousContext = tabContext;
|
||||
}
|
||||
}
|
||||
|
||||
VSS.ready(function () {
|
||||
registerContribution(VSS.getContribution());
|
||||
|
||||
if (isBackground()) {
|
||||
VSS.notifyLoadSucceeded();
|
||||
} else {
|
||||
// Load main entry point for extension
|
||||
VSS.require(["FeatureTimeline"], function (ft) {
|
||||
FeatureTimeline = ft;
|
||||
FeatureTimeline.initialize();
|
||||
// loading succeeded
|
||||
VSS.notifyLoadSucceeded();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function isBackground() {
|
||||
const contributionContext = VSS.getConfiguration();
|
||||
return contributionContext && contributionContext.host && contributionContext.host.background;
|
||||
}
|
||||
</script>
|
||||
<div id="root" />
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,94 @@
|
|||
export function iePollyfill() {
|
||||
if (!Array.prototype.find) {
|
||||
Object.defineProperty(Array.prototype, 'find', {
|
||||
value: function (predicate) {
|
||||
// 1. Let O be ? ToObject(this value).
|
||||
if (this == null) {
|
||||
throw new TypeError('"this" is null or not defined');
|
||||
}
|
||||
|
||||
var o = Object(this);
|
||||
|
||||
// 2. Let len be ? ToLength(? Get(O, "length")).
|
||||
var len = o.length >>> 0;
|
||||
|
||||
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
|
||||
if (typeof predicate !== 'function') {
|
||||
throw new TypeError('predicate must be a function');
|
||||
}
|
||||
|
||||
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
var thisArg = arguments[1];
|
||||
|
||||
// 5. Let k be 0.
|
||||
var k = 0;
|
||||
|
||||
// 6. Repeat, while k < len
|
||||
while (k < len) {
|
||||
// a. Let Pk be ! ToString(k).
|
||||
// b. Let kValue be ? Get(O, Pk).
|
||||
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
|
||||
// d. If testResult is true, return kValue.
|
||||
var kValue = o[k];
|
||||
if (predicate.call(thisArg, kValue, k, o)) {
|
||||
return kValue;
|
||||
}
|
||||
// e. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 7. Return undefined.
|
||||
return undefined;
|
||||
},
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
}
|
||||
|
||||
// https://tc39.github.io/ecma262/#sec-array.prototype.findIndex
|
||||
if (!Array.prototype.findIndex) {
|
||||
Object.defineProperty(Array.prototype, 'findIndex', {
|
||||
value: function (predicate) {
|
||||
// 1. Let O be ? ToObject(this value).
|
||||
if (this == null) {
|
||||
throw new TypeError('"this" is null or not defined');
|
||||
}
|
||||
|
||||
var o = Object(this);
|
||||
|
||||
// 2. Let len be ? ToLength(? Get(O, "length")).
|
||||
var len = o.length >>> 0;
|
||||
|
||||
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
|
||||
if (typeof predicate !== 'function') {
|
||||
throw new TypeError('predicate must be a function');
|
||||
}
|
||||
|
||||
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
var thisArg = arguments[1];
|
||||
|
||||
// 5. Let k be 0.
|
||||
var k = 0;
|
||||
|
||||
// 6. Repeat, while k < len
|
||||
while (k < len) {
|
||||
// a. Let Pk be ! ToString(k).
|
||||
// b. Let kValue be ? Get(O, Pk).
|
||||
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
|
||||
// d. If testResult is true, return k.
|
||||
var kValue = o[k];
|
||||
if (predicate.call(thisArg, kValue, k, o)) {
|
||||
return k;
|
||||
}
|
||||
// e. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 7. Return -1.
|
||||
return -1;
|
||||
},
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { DropTarget } from 'react-dnd';
|
||||
import { IIterationSahdowProps, IterationShadow } from './IterationShadow';
|
||||
import React = require('react');
|
||||
import { IWorkItemRendererProps } from './WorkItem/WorkItemRenderer';
|
||||
|
||||
|
||||
export class DroppableIterationShadow extends React.Component<IIterationSahdowProps, {}> {
|
||||
public render() {
|
||||
return <IterationShadow {...this.props} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const iterationDropTarget = {
|
||||
canDrop(dropTargetProps: IIterationSahdowProps, monitor) {
|
||||
// let item = monitor.getItem() as IDraggableWorkItemRendererProps;
|
||||
// item = null;
|
||||
return true;
|
||||
},
|
||||
drop(dropTargetProps: IIterationSahdowProps, monitor, component) {
|
||||
if (monitor.didDrop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedItem = monitor.getItem() as IWorkItemRendererProps;
|
||||
|
||||
dropTargetProps.changeIteration(draggedItem.id, dropTargetProps.teamIteration, draggedItem.allowOverride);
|
||||
|
||||
return { moved: true };
|
||||
}
|
||||
}
|
||||
|
||||
function collect(connect, monitor) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
// You can ask the monitor about the current drag state:
|
||||
isOver: monitor.isOver(),
|
||||
isOverCurrent: monitor.isOver({ shallow: true }),
|
||||
canDrop: monitor.canDrop(),
|
||||
itemType: monitor.getItemType()
|
||||
};
|
||||
}
|
||||
|
||||
export const IterationDropTarget = DropTarget("WorkItem", iterationDropTarget, collect)(DroppableIterationShadow);
|
|
@ -0,0 +1,75 @@
|
|||
.feature-timeline-main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.root-container {
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
display: -ms-grid;
|
||||
display: grid;
|
||||
grid-column-gap: 5px;
|
||||
}
|
||||
|
||||
.feature-container {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@mixin button-dimension {
|
||||
height: 20px;
|
||||
padding: 3px;
|
||||
margin: 2px;
|
||||
border-radius: 5px;
|
||||
border: solid 1px lightgray;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.command {
|
||||
@include button-dimension();
|
||||
background: lightgray;
|
||||
color: black;
|
||||
overflow: ellipse;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
@include button-dimension();
|
||||
background: lightgray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.non-button {
|
||||
@include button-dimension();
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.iteration-options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.command-right-section,
|
||||
.last-header-column-command {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.non-button,
|
||||
.button {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.single-column-commands {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
import * as React from 'react';
|
||||
import configureStore from '../../redux/configureStore';
|
||||
import DraggableWorkItemRenderer from './WorkItem/DraggableWorkItemRenderer';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import {
|
||||
changeDisplayIterationCount,
|
||||
displayAllIterations,
|
||||
shiftDisplayIterationLeft,
|
||||
shiftDisplayIterationRight
|
||||
} from '../../redux/store/teamiterations/actionCreators';
|
||||
import { clearOverrideIteration, launchWorkItemForm, startUpdateWorkItemIteration } from '../../redux/store/workitems/actionCreators';
|
||||
import { closeDetails, createInitialize, showDetails } from '../../redux/store/common/actioncreators';
|
||||
import { connect, Provider } from 'react-redux';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import { endOverrideIteration, overrideHoverOverIteration, startOverrideIteration } from '../../redux/store/overrideIterationProgress/actionCreators';
|
||||
import {
|
||||
getBacklogLevel,
|
||||
getProjectId,
|
||||
getRawState,
|
||||
getTeamId,
|
||||
gridViewSelector,
|
||||
uiStatusSelector
|
||||
} from '../../redux/selectors';
|
||||
import { getRowColumnStyle, getTemplateColumns } from './gridhelper';
|
||||
import { IFeatureTimelineRawState, IWorkItemOverrideIteration } from '../../redux/store';
|
||||
import { IGridView } from '../../redux/selectors/gridViewSelector';
|
||||
import { IterationDropTarget } from './DroppableIterationShadow';
|
||||
import { IterationRenderer } from './IterationRenderer';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
|
||||
import { TimelineDialog } from './TimelineDialog';
|
||||
import { UIStatus } from '../../redux/types';
|
||||
import { WorkitemGap } from './WorkItem/WorkItemGap';
|
||||
import { WorkItemShadow } from './WorkItem/WorkItemShadow';
|
||||
import './FeatureTimelineGrid.scss';
|
||||
|
||||
export interface IFeatureTimelineGridProps {
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
rawState: IFeatureTimelineRawState;
|
||||
uiState: UIStatus;
|
||||
gridView: IGridView,
|
||||
childItems: number[];
|
||||
launchWorkItemForm: (id: number) => void;
|
||||
showDetails: (id: number) => void;
|
||||
closeDetails: (id: number) => void;
|
||||
clearOverrideIteration: (id: number) => void;
|
||||
dragHoverOverIteration: (iteration: string) => void;
|
||||
overrideIterationStart: (payload: IWorkItemOverrideIteration) => void;
|
||||
overrideIterationEnd: () => void;
|
||||
changeIteration: (id: number, teamIteration: TeamSettingsIteration, override: boolean) => void;
|
||||
showThreeIterations: (projectId: string, teamId: string) => void;
|
||||
showFiveIterations: (projectId: string, teamId: string) => void;
|
||||
shiftDisplayIterationLeft: () => void;
|
||||
shiftDisplayIterationRight: () => void;
|
||||
showAllIterations: () => void;
|
||||
}
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
return (state: IFeatureTimelineRawState) => {
|
||||
return {
|
||||
projectId: getProjectId(),
|
||||
teamId: getTeamId(),
|
||||
rawState: getRawState(state),
|
||||
uiState: uiStatusSelector()(state),
|
||||
gridView: gridViewSelector()(state),
|
||||
childItems: state.workItemDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
launchWorkItemForm: (id: number) => {
|
||||
if (id) {
|
||||
dispatch(launchWorkItemForm(id));
|
||||
}
|
||||
},
|
||||
showDetails: (id: number) => {
|
||||
dispatch(showDetails(id));
|
||||
},
|
||||
closeDetails: (id: number) => {
|
||||
dispatch(closeDetails(id));
|
||||
},
|
||||
dragHoverOverIteration: (iterationId: string) => {
|
||||
dispatch(overrideHoverOverIteration(iterationId));
|
||||
},
|
||||
overrideIterationStart: (payload: IWorkItemOverrideIteration) => {
|
||||
dispatch(startOverrideIteration(payload));
|
||||
},
|
||||
overrideIterationEnd: () => {
|
||||
dispatch(endOverrideIteration());
|
||||
},
|
||||
clearOverrideIteration: (id: number) => {
|
||||
dispatch(clearOverrideIteration(id));
|
||||
},
|
||||
changeIteration: (id: number, teamIteration: TeamSettingsIteration, override: boolean) => {
|
||||
dispatch(startUpdateWorkItemIteration([id], teamIteration, override));
|
||||
},
|
||||
showThreeIterations: (projectId: string, teamId: string) => {
|
||||
dispatch(changeDisplayIterationCount(3, projectId, teamId));
|
||||
},
|
||||
showFiveIterations: (projectId: string, teamId: string) => {
|
||||
dispatch(changeDisplayIterationCount(5, projectId, teamId));
|
||||
},
|
||||
showAllIterations: () => {
|
||||
dispatch(displayAllIterations());
|
||||
},
|
||||
shiftDisplayIterationLeft: () => {
|
||||
dispatch(shiftDisplayIterationLeft(1));
|
||||
},
|
||||
shiftDisplayIterationRight: () => {
|
||||
dispatch(shiftDisplayIterationRight(1));
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
interface IFeatureTimelineGridState {
|
||||
collapsedGroups: IDictionaryNumberTo<boolean>;
|
||||
}
|
||||
|
||||
export class FeatureTimelineGrid extends React.Component<IFeatureTimelineGridProps, IFeatureTimelineGridState> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
collapsedGroups: {}
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
||||
const {
|
||||
uiState,
|
||||
projectId,
|
||||
teamId
|
||||
} = this.props;
|
||||
if (!this.props.rawState || uiState === UIStatus.Loading) {
|
||||
return (
|
||||
<Spinner size={SpinnerSize.large} label="Loading..." />
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.rawState.error) {
|
||||
return (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
>
|
||||
{this.props.rawState.error}
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
if (uiState === UIStatus.NoTeamIterations) {
|
||||
return (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
>
|
||||
{"The team does not have any iteration selected, please visit team admin page and select team iterations."}
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
if (uiState === UIStatus.NoWorkItems) {
|
||||
return (<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
isMultiline={false}
|
||||
>
|
||||
{"No in-progress Features for the timeline."}
|
||||
</MessageBar>);
|
||||
}
|
||||
|
||||
const {
|
||||
emptyHeaderRow,
|
||||
iterationHeader,
|
||||
iterationShadow,
|
||||
workItems,
|
||||
workItemShadow,
|
||||
iterationDisplayOptions,
|
||||
isSubGrid
|
||||
} = this.props.gridView;
|
||||
|
||||
const columnHeading = iterationHeader.map((iteration, index) => {
|
||||
const style = getRowColumnStyle(iteration.dimension);
|
||||
return (
|
||||
<div className="columnheading" style={style}>
|
||||
<IterationRenderer iteration={iteration.teamIteration} />
|
||||
</div>
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
const shadows = iterationShadow.map((shadow, index) => {
|
||||
return (
|
||||
<IterationDropTarget
|
||||
{...shadow}
|
||||
isOverrideIterationInProgress={!!this.props.rawState.workItemOverrideIteration}
|
||||
onOverrideIterationOver={this.props.dragHoverOverIteration.bind(this)}
|
||||
changeIteration={this.props.changeIteration.bind(this)}
|
||||
>
|
||||
|
||||
</IterationDropTarget>
|
||||
);
|
||||
});
|
||||
|
||||
let workItemShadowCell = null;
|
||||
if (workItemShadow) {
|
||||
const workItem = workItems.filter(w => !w.isGap && w.workItem.id === workItemShadow)[0];
|
||||
workItemShadowCell = (
|
||||
<WorkItemShadow {...workItem.dimension} />
|
||||
);
|
||||
}
|
||||
|
||||
const workItemCells = workItems.filter(w => !w.isGap && w.workItem.id).map(w => {
|
||||
return (
|
||||
<DraggableWorkItemRenderer
|
||||
id={w.workItem.id}
|
||||
title={w.workItem.title}
|
||||
color={w.workItem.color}
|
||||
isRoot={w.workItem.isRoot}
|
||||
iterationDuration={w.workItem.iterationDuration}
|
||||
dimension={w.dimension}
|
||||
onClick={id => this.props.launchWorkItemForm(id)}
|
||||
shouldShowDetails={w.workItem.shouldShowDetails}
|
||||
showDetails={id => this.props.showDetails(id)}
|
||||
overrideIterationStart={payload => this.props.overrideIterationStart(payload)}
|
||||
overrideIterationEnd={() => this.props.overrideIterationEnd()}
|
||||
allowOverride={!this.props.gridView.isSubGrid}
|
||||
crop={w.crop}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const workItemGaps = workItems.filter(w => w.isGap).map(w => {
|
||||
return (
|
||||
<WorkitemGap {...w.dimension} />
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const extraColumns = this.props.gridView.hideParents ? [] : ['minmax(100px, 10%)'];
|
||||
const gridStyle = getTemplateColumns(extraColumns, shadows.length, 'minmax(10%, 400px)');
|
||||
|
||||
let childDialog = null;
|
||||
if (this.props.childItems.length > 0) {
|
||||
const props = { ...this.props, id: this.props.childItems[0] };
|
||||
childDialog = <TimelineDialog {...props} />
|
||||
}
|
||||
|
||||
let leftButton = <span className="non-button"></span>;
|
||||
if (iterationDisplayOptions && iterationDisplayOptions.startIndex > 0) {
|
||||
leftButton = <span className="button" onClick={() => this.props.shiftDisplayIterationLeft()}>{"<<"}</span>;
|
||||
}
|
||||
|
||||
let rightButton = <span className="non-button"></span>;
|
||||
if (iterationDisplayOptions && iterationDisplayOptions.endIndex < (iterationDisplayOptions.totalIterations - 1)) {
|
||||
rightButton = <span className="button" onClick={() => this.props.shiftDisplayIterationRight()}>{">>"}</span>
|
||||
}
|
||||
|
||||
let displayOptions = null;
|
||||
let commandHeading = [];
|
||||
|
||||
if (!isSubGrid && (iterationDisplayOptions || columnHeading.length > 3)) {
|
||||
displayOptions = (
|
||||
<div className="iteration-options">
|
||||
<span className="command" onClick={() => this.props.showThreeIterations(projectId, teamId)}>Show three Sprints</span>
|
||||
<span className="command" onClick={() => this.props.showFiveIterations(projectId, teamId)}>Show five Sprints</span>
|
||||
<span className="command" onClick={() => this.props.showAllIterations()}>Show all sprints</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (emptyHeaderRow.length === 1) {
|
||||
// Special case only one column
|
||||
let rowColumnStyle = getRowColumnStyle(emptyHeaderRow[0]);
|
||||
const commands = (
|
||||
<div style={rowColumnStyle} className="single-column-commands">
|
||||
<div className="command-left-section">
|
||||
{leftButton}
|
||||
</div>
|
||||
<div className="command-right-section">
|
||||
{rightButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
commandHeading.push(commands);
|
||||
|
||||
} else {
|
||||
// Add left button to first empty heading cell
|
||||
let rowColumnStyle = getRowColumnStyle(emptyHeaderRow[0]);
|
||||
const firstHeaderColumnCommand = (
|
||||
<div style={rowColumnStyle} className="first-header-column-command">
|
||||
{leftButton}
|
||||
</div>
|
||||
);
|
||||
commandHeading.push(firstHeaderColumnCommand);
|
||||
|
||||
// Add display options and right button on last empty heading cell
|
||||
rowColumnStyle = getRowColumnStyle(emptyHeaderRow[emptyHeaderRow.length - 1]);
|
||||
const lastHeaderColumnCommand = (
|
||||
<div style={rowColumnStyle} className="last-header-column-command">
|
||||
{rightButton}
|
||||
</div>
|
||||
);
|
||||
commandHeading.push(lastHeaderColumnCommand);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="root-container">
|
||||
{displayOptions}
|
||||
<div className="feature-timeline-main-container">
|
||||
<div className="container" style={gridStyle}>
|
||||
{commandHeading}
|
||||
{columnHeading}
|
||||
{shadows}
|
||||
{workItemShadowCell}
|
||||
{workItemCells}
|
||||
{workItemGaps}
|
||||
{childDialog}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ConntectedFeatureTimeline = connect(
|
||||
makeMapStateToProps, mapDispatchToProps
|
||||
)(FeatureTimelineGrid);
|
||||
|
||||
export const PrimaryGrid = () => {
|
||||
const initialState: IFeatureTimelineRawState = {
|
||||
loading: true
|
||||
} as IFeatureTimelineRawState;
|
||||
const store = configureStore(initialState);
|
||||
|
||||
const projectId = getProjectId();
|
||||
const teamId = getTeamId();
|
||||
const backlogLevel = getBacklogLevel();
|
||||
|
||||
const action = createInitialize(projectId, teamId, backlogLevel);
|
||||
store.dispatch(action);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ConntectedFeatureTimeline />
|
||||
</Provider>);
|
||||
}
|
||||
|
||||
export const DragDropGrid = DragDropContext(HTML5Backend)(PrimaryGrid);
|
|
@ -0,0 +1,10 @@
|
|||
.info-icon {
|
||||
color: transparent !important;
|
||||
cursor: pointer;
|
||||
margin-top: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
color: white !important;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import './InfoIcon.scss';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IInfoIconProps {
|
||||
id: number;
|
||||
onClick: (id: number) => void;
|
||||
}
|
||||
|
||||
export class InfoIcon extends React.Component<IInfoIconProps, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="bowtie-icon bowtie-status-info info-icon"
|
||||
onClick={() => this.props.onClick(this.props.id)}>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
.iteration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f4f4;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
border: 1px solid lightgrey;
|
||||
font-family: "Segoe UI VSS (Regular)", "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
height: 100%;
|
||||
.iterationname {
|
||||
color: #212121;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
}
|
||||
.dates {
|
||||
color: #666666;
|
||||
font-size: 11px;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.current-sprint-marker {
|
||||
margin-left: 3px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 2px;
|
||||
background-color: rgb(16, 110, 190);
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
width: 100px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import "./IterationRenderer.scss";
|
||||
import * as React from "react";
|
||||
import { TeamSettingsIteration, TimeFrame } from "TFS/Work/Contracts";
|
||||
|
||||
export interface IIterationRendererProps {
|
||||
iteration: TeamSettingsIteration;
|
||||
|
||||
}
|
||||
|
||||
function getMMDD(date: Date) {
|
||||
var mm = date.getMonth() < 9 ? "0" + (date.getMonth() + 1) : (date.getMonth() + 1); // getMonth() is zero-based
|
||||
var dd = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
|
||||
return `${mm}/${dd}`;
|
||||
}
|
||||
|
||||
export class IterationRenderer extends React.Component<IIterationRendererProps, {}> {
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
iteration
|
||||
} = this.props;
|
||||
|
||||
// TODO: Start and end date conversion?
|
||||
const startDate = iteration.attributes["startDate"] ? getMMDD(new Date(iteration.attributes["startDate"])) : null;
|
||||
const endDate = iteration.attributes["finishDate"] ? getMMDD(new Date(iteration.attributes["finishDate"])) : null;
|
||||
|
||||
let dates: JSX.Element = null;
|
||||
if (startDate && endDate) {
|
||||
dates = (
|
||||
<div className="dates">
|
||||
{`${startDate} - ${endDate}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let currentMarker = null;
|
||||
if (TimeFrame && iteration.attributes.timeFrame === TimeFrame.Current) {
|
||||
currentMarker = (
|
||||
<span className="current-sprint-marker">Current</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="iteration">
|
||||
<div className="iterationname">
|
||||
<span>{iteration.name}</span>
|
||||
{currentMarker}
|
||||
</div>
|
||||
{dates}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.columnshadow {
|
||||
margin-top: 2px;
|
||||
background: #f4f4f4;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
border: 1px solid yellowgreen;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import './IterationShadow.scss';
|
||||
|
||||
import * as React from 'react';
|
||||
import { IGridIteration } from '../../redux/selectors/gridViewSelector';
|
||||
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
|
||||
import { getRowColumnStyle } from './gridhelper';
|
||||
|
||||
export interface IIterationSahdowProps extends IGridIteration {
|
||||
isOverrideIterationInProgress: boolean;
|
||||
onOverrideIterationOver: (iteration: string) => void;
|
||||
changeIteration: (id: number, teamIteration: TeamSettingsIteration, override: boolean) => void;
|
||||
|
||||
connectDropTarget?: (element: JSX.Element) => JSX.Element;
|
||||
isOver?: boolean;
|
||||
canDrop?: () => boolean;
|
||||
}
|
||||
|
||||
export interface IIterationSahdowState {
|
||||
shouldHighlight: boolean;
|
||||
}
|
||||
|
||||
export class IterationShadow extends React.Component<IIterationSahdowProps, IIterationSahdowState> {
|
||||
|
||||
private _div: HTMLDivElement;
|
||||
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
shouldHighlight: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public render() {
|
||||
const className = "columnshadow" + (this.state.shouldHighlight || this.props.isOver ? " highlight" : "");
|
||||
const style = getRowColumnStyle(this.props.dimension);
|
||||
const { connectDropTarget } = this.props;
|
||||
|
||||
return connectDropTarget(
|
||||
<div
|
||||
className={className}
|
||||
ref={(e) => this._div = e}
|
||||
onMouseMove={this._onMouseEnter}
|
||||
onDragOver={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onMouseEnter = () => {
|
||||
if (this.props.isOverrideIterationInProgress) {
|
||||
this.setState({
|
||||
shouldHighlight: true
|
||||
});
|
||||
|
||||
this.props.onOverrideIterationOver(this.props.teamIteration.id);
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseLeave = () => {
|
||||
if (this.props.isOverrideIterationInProgress)
|
||||
this.setState({
|
||||
shouldHighlight: false
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
.timeline-dialog {
|
||||
max-width: 60% !important;
|
||||
}
|
||||
|
||||
.timeline-dialog .container {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.custom-duration-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
.div {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-grid-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-duration-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-duration-iterations {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.custom-duration-iteration {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import './TimelineDialog.scss'
|
||||
import * as React from 'react';
|
||||
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { FeatureTimelineGrid, IFeatureTimelineGridProps } from './FeatureTimelineGrid';
|
||||
import { getGridView } from '../../redux/selectors/gridViewSelector';
|
||||
import { getTeamIterations } from '../../redux/selectors/teamIterations';
|
||||
import { IterationDurationKind } from '../../redux/store';
|
||||
import { IterationRenderer } from './IterationRenderer';
|
||||
import { Button, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
|
||||
export interface ITimelineDialogProps extends IFeatureTimelineGridProps {
|
||||
id: number;
|
||||
clearOverrideIteration: (id: number) => void;
|
||||
}
|
||||
|
||||
export class TimelineDialog extends React.Component<ITimelineDialogProps, {}> {
|
||||
public render() {
|
||||
const gridWorkItem = this._getGridWorkItem();
|
||||
let dialogDetails = null;
|
||||
let footer = null;
|
||||
switch (gridWorkItem.workItem.iterationDuration.kind) {
|
||||
case IterationDurationKind.UserOverridden:
|
||||
dialogDetails = this._getCustomIterationDurationDetails();
|
||||
break;
|
||||
default:
|
||||
dialogDetails = this._getChildrenFeatureTimelineGrid();
|
||||
footer = (<DialogFooter>
|
||||
<div>
|
||||
<PrimaryButton onClick={() => this.props.closeDetails(this.props.id)}>Close</PrimaryButton>
|
||||
</div>
|
||||
</DialogFooter>)
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
hidden={false}
|
||||
onDismiss={() => this.props.closeDetails(this.props.id)}
|
||||
dialogContentProps={
|
||||
{
|
||||
type: DialogType.close,
|
||||
title: gridWorkItem.workItem.title
|
||||
}
|
||||
}
|
||||
modalProps={
|
||||
{
|
||||
isBlocking: true,
|
||||
containerClassName: "timeline-dialog"
|
||||
}
|
||||
}
|
||||
>
|
||||
{dialogDetails}
|
||||
{footer}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
private _getChildrenFeatureTimelineGrid() {
|
||||
const gridWorkItem = this._getGridWorkItem();
|
||||
const gridView = getGridView(
|
||||
this.props.uiState,
|
||||
getTeamIterations(this.props.projectId, this.props.teamId, this.props.uiState, this.props.rawState),
|
||||
[gridWorkItem.workItem],
|
||||
/* workItemOverrideIteration */ null,
|
||||
/* iterationDisplayOptions */ null,
|
||||
/* isSubGrid */ true);
|
||||
|
||||
const childItems = this.props.childItems.filter(id => id !== this.props.id);
|
||||
|
||||
const props = { ...this.props, gridView, childItems };
|
||||
if (gridView.workItems.length > 0) {
|
||||
return (
|
||||
<FeatureTimelineGrid {...props}>
|
||||
</FeatureTimelineGrid>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _getGridWorkItem() {
|
||||
return this.props.gridView.workItems.filter(w => !w.isGap && w.workItem.id === this.props.id)[0];
|
||||
}
|
||||
|
||||
private _getCustomIterationDurationDetails() {
|
||||
const gridWorkItem = this._getGridWorkItem();
|
||||
const {
|
||||
overridedBy,
|
||||
startIteration,
|
||||
endIteration
|
||||
} = gridWorkItem.workItem.iterationDuration;
|
||||
|
||||
const title = `${overridedBy} has set following start and end iteration for this workitem.`;
|
||||
|
||||
return (
|
||||
<div className="dialog-contents">
|
||||
<div className="dialog-grid-container">
|
||||
{this._getChildrenFeatureTimelineGrid()}
|
||||
</div>
|
||||
|
||||
<div className="custom-duration-container">
|
||||
<div className="custom-duration-title">
|
||||
{title}
|
||||
</div>
|
||||
<div className="custom-duration-iterations">
|
||||
<div className="custom-duration-iteration text">
|
||||
{"Start Iteration"}
|
||||
</div>
|
||||
<div className="custom-duration-iteration text">
|
||||
{"End Iteration"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-duration-iterations">
|
||||
<div className="custom-duration-iteration">
|
||||
<IterationRenderer iteration={startIteration} />
|
||||
</div>
|
||||
<div className="custom-duration-iteration">
|
||||
<IterationRenderer iteration={endIteration} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="custom-duration-footer">
|
||||
<div>
|
||||
<Button onClick={this._onClear}>Clear</Button>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton onClick={() => this.props.closeDetails(this.props.id)}>Close</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
private _onClear = () => {
|
||||
this.props.closeDetails(this.props.id);
|
||||
this.props.clearOverrideIteration(this.props.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { DragSource } from 'react-dnd';
|
||||
import { IWorkItemRendererProps, WorkItemRenderer } from './WorkItemRenderer';
|
||||
import * as React from 'react';
|
||||
|
||||
class DraggableHelper extends React.Component<IWorkItemRendererProps, {}> {
|
||||
public render() {
|
||||
return <WorkItemRenderer {...this.props} />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const WorkItemSource = {
|
||||
beginDrag(props: IWorkItemRendererProps) {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the props to inject into your component.
|
||||
*/
|
||||
function collect(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
export default DragSource("WorkItem", WorkItemSource, collect)(DraggableHelper);
|
|
@ -0,0 +1,16 @@
|
|||
import "./WorkItemRenderer.scss";
|
||||
|
||||
import * as React from 'react';
|
||||
import { IDimension } from '../../../redux/types';
|
||||
import { getRowColumnStyle } from '../gridhelper';
|
||||
|
||||
export class WorkitemGap extends React.Component<IDimension, {}> {
|
||||
public render() {
|
||||
const style = getRowColumnStyle(this.props);
|
||||
return (
|
||||
<div className="work-item-gap" style={style}>
|
||||
<div className="title"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
@mixin default {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 15px;
|
||||
padding: 2px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
@mixin work-item-iteration-indicator {
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
opacity: 0.7;
|
||||
color: black;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-bottom: 2px;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
|
||||
}
|
||||
|
||||
.work-item-start-iteration-indicator {
|
||||
@include work-item-iteration-indicator;
|
||||
border-top-left-radius: 50px;
|
||||
border-bottom-left-radius: 50px;
|
||||
}
|
||||
|
||||
.work-item-end-iteration-indicator {
|
||||
@include work-item-iteration-indicator;
|
||||
border-top-right-radius: 50px;
|
||||
border-bottom-right-radius: 50px;
|
||||
}
|
||||
|
||||
.small-border {
|
||||
width: 5px;
|
||||
background: transparent;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.work-item-details-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title-without-infoicon {
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.title-with-infoicon {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.title-contents {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
padding-bottom: 2px;
|
||||
flex-grow: 1 0 auto;
|
||||
}
|
||||
|
||||
.work-item-shadow {
|
||||
@include default;
|
||||
}
|
||||
|
||||
.work-item-details-container:hover+.info-icon {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.work-item {
|
||||
@include default;
|
||||
}
|
||||
|
||||
.root-work-item {
|
||||
@include default;
|
||||
margin-top: 5px;
|
||||
border-radius: 5px;
|
||||
height: calc(100% - 5px);
|
||||
}
|
||||
|
||||
.work-item-gap {
|
||||
margin-top: 10px;
|
||||
border: 1px solid lightgrey;
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
import './WorkItemRenderer.scss';
|
||||
import * as React from 'react';
|
||||
import { InfoIcon } from '../InfoIcon/InfoIcon';
|
||||
import { IIterationDuration, IWorkItemOverrideIteration } from '../../../redux/store';
|
||||
import { IDimension, CropWorkItem } from '../../../redux/types';
|
||||
import { getRowColumnStyle } from '../gridhelper';
|
||||
import {
|
||||
TooltipHost, TooltipOverflowMode
|
||||
} from 'office-ui-fabric-react/lib/Tooltip';
|
||||
import { css } from '@uifabric/utilities/lib/css';
|
||||
export interface IWorkItemRendererProps {
|
||||
id: number;
|
||||
title: string;
|
||||
color: string;
|
||||
isRoot: boolean;
|
||||
shouldShowDetails: boolean;
|
||||
allowOverride: boolean;
|
||||
iterationDuration: IIterationDuration;
|
||||
dimension: IDimension;
|
||||
crop: CropWorkItem;
|
||||
onClick: (id: number) => void;
|
||||
showDetails: (id: number) => void;
|
||||
overrideIterationStart: (payload: IWorkItemOverrideIteration) => void;
|
||||
overrideIterationEnd: () => void;
|
||||
|
||||
isDragging?: boolean;
|
||||
connectDragSource?: (element: JSX.Element) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface IWorkItemRendrerState {
|
||||
left: number;
|
||||
width: number;
|
||||
top: number;
|
||||
height: number;
|
||||
resizing: boolean;
|
||||
isLeft: boolean;
|
||||
}
|
||||
|
||||
export class WorkItemRenderer extends React.Component<IWorkItemRendererProps, IWorkItemRendrerState> {
|
||||
private _div: HTMLDivElement;
|
||||
private _origPageX: number;
|
||||
private _origWidth: number;
|
||||
|
||||
public constructor(props: IWorkItemRendererProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
resizing: false
|
||||
} as IWorkItemRendrerState;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
onClick,
|
||||
showDetails,
|
||||
isRoot,
|
||||
shouldShowDetails,
|
||||
allowOverride,
|
||||
isDragging,
|
||||
crop,
|
||||
iterationDuration
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
resizing,
|
||||
left,
|
||||
top,
|
||||
height,
|
||||
width
|
||||
} = this.state
|
||||
|
||||
let style = {};
|
||||
|
||||
if (!resizing) {
|
||||
style = getRowColumnStyle(this.props.dimension);
|
||||
} else {
|
||||
style['position'] = 'fixed';
|
||||
style['left'] = left + "px";
|
||||
style['width'] = width + "px";
|
||||
style['top'] = top + "px";
|
||||
style['height'] = height + "px";
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
style['background'] = hexToRgb(this.props.color, 0.1);
|
||||
} else {
|
||||
style['background'] = hexToRgb(this.props.color, 0.8);
|
||||
}
|
||||
|
||||
const className = isRoot ? "root-work-item" : "work-item";
|
||||
let cropClassName = "crop-none";
|
||||
let canOverrideLeft = allowOverride;
|
||||
let canOverrideRight = allowOverride;
|
||||
let leftCropped = false;
|
||||
let rightCropped = false;
|
||||
switch (crop) {
|
||||
case CropWorkItem.Left: {
|
||||
cropClassName = "crop-left";
|
||||
canOverrideLeft = false;
|
||||
leftCropped = true;
|
||||
break;
|
||||
}
|
||||
case CropWorkItem.Right: {
|
||||
cropClassName = "crop-right";
|
||||
canOverrideRight = false;
|
||||
rightCropped = true;
|
||||
break;
|
||||
}
|
||||
case CropWorkItem.Both: {
|
||||
cropClassName = "crop-both";
|
||||
canOverrideLeft = false;
|
||||
canOverrideRight = false;
|
||||
leftCropped = true;
|
||||
rightCropped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const infoIcon = shouldShowDetails ? <InfoIcon id={id} onClick={id => showDetails(id)} /> : null;
|
||||
const additionalTitleClass = infoIcon ? "title-with-infoicon" : "title-without-infoicon";
|
||||
|
||||
let leftHandle = null;
|
||||
let rightHandle = null;
|
||||
|
||||
if (!isRoot && allowOverride) {
|
||||
leftHandle = canOverrideLeft && (
|
||||
<div
|
||||
className="small-border"
|
||||
onMouseDown={this._leftMouseDown}
|
||||
onMouseUp={this._mouseUp}
|
||||
/>
|
||||
);
|
||||
|
||||
rightHandle = canOverrideRight && (
|
||||
<div
|
||||
className="small-border"
|
||||
onMouseDown={this._rightMouseDown}
|
||||
onMouseUp={this._mouseUp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let startsFrom = <div />;
|
||||
if (leftCropped) {
|
||||
startsFrom = (<TooltipHost
|
||||
content={`Starts at ${iterationDuration.startIteration.name}`}>
|
||||
<div className="work-item-start-iteration-indicator">{`${iterationDuration.startIteration.name}`}</div>
|
||||
</TooltipHost>
|
||||
);
|
||||
}
|
||||
|
||||
let endsAt = <div />;
|
||||
if (rightCropped) {
|
||||
endsAt = (<TooltipHost
|
||||
content={`Ends at ${iterationDuration.endIteration.name}`}>
|
||||
<div className="work-item-end-iteration-indicator">{`${iterationDuration.endIteration.name}`}</div>
|
||||
</TooltipHost>
|
||||
);
|
||||
}
|
||||
|
||||
const item = (
|
||||
<div style={style}
|
||||
className={css(className, cropClassName)}
|
||||
ref={(e) => this._div = e}
|
||||
>
|
||||
{leftHandle}
|
||||
<div
|
||||
className={css("work-item-details-container", additionalTitleClass)}
|
||||
>
|
||||
{startsFrom}
|
||||
<div
|
||||
className="title-contents"
|
||||
onClick={() => onClick(id)}
|
||||
>
|
||||
<TooltipHost
|
||||
content={title}
|
||||
overflowMode={TooltipOverflowMode.Parent}
|
||||
>
|
||||
{title}
|
||||
</TooltipHost>
|
||||
</div>
|
||||
{endsAt}
|
||||
</div>
|
||||
{infoIcon}
|
||||
{rightHandle}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isRoot) {
|
||||
return item;
|
||||
}
|
||||
const { connectDragSource } = this.props;
|
||||
|
||||
return connectDragSource(item);
|
||||
}
|
||||
|
||||
private _leftMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
this._resizeStart(e, true);
|
||||
}
|
||||
|
||||
private _rightMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
this._resizeStart(e, false);
|
||||
}
|
||||
|
||||
private _mouseUp = () => {
|
||||
window.removeEventListener("mousemove", this._mouseMove);
|
||||
window.removeEventListener("mouseup", this._mouseUp);
|
||||
this.setState({
|
||||
resizing: false
|
||||
});
|
||||
|
||||
this.props.overrideIterationEnd();
|
||||
}
|
||||
|
||||
private _resizeStart(e: React.MouseEvent<HTMLDivElement>, isLeft: boolean) {
|
||||
e.preventDefault();
|
||||
const rect = this._div.getBoundingClientRect() as ClientRect;
|
||||
this._origPageX = e.pageX;
|
||||
this._origWidth = rect.width;
|
||||
|
||||
this.props.overrideIterationStart({
|
||||
workItemId: this.props.id,
|
||||
iterationDuration: {
|
||||
startIterationId: this.props.iterationDuration.startIteration.id,
|
||||
endIterationId: this.props.iterationDuration.endIteration.id,
|
||||
user: VSS.getWebContext().user.uniqueName
|
||||
},
|
||||
changingStart: isLeft
|
||||
});
|
||||
|
||||
window.addEventListener("mousemove", this._mouseMove);
|
||||
window.addEventListener("mouseup", this._mouseUp);
|
||||
|
||||
this.setState({
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
top: rect.top - 10, //The rect.top does not contain margin-top
|
||||
height: rect.height,
|
||||
resizing: true,
|
||||
isLeft: isLeft
|
||||
});
|
||||
}
|
||||
|
||||
private _mouseMove = (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
const newPageX = ev.pageX;
|
||||
if (this.state.isLeft) {
|
||||
let width = 0;
|
||||
// moved mouse left we need to increase the width
|
||||
if (newPageX < this._origPageX) {
|
||||
width = this._origWidth + (this._origPageX - newPageX);
|
||||
} else {
|
||||
// moved mouse right we need to decrease the width
|
||||
width = this._origWidth - (newPageX - this._origPageX);
|
||||
}
|
||||
|
||||
if (width > 100) {
|
||||
this.setState({
|
||||
left: ev.clientX,
|
||||
width: width
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let width = 0;
|
||||
// movd left we need to decrease the width
|
||||
if (newPageX < this._origPageX) {
|
||||
width = this._origWidth - (this._origPageX - newPageX);
|
||||
} else {
|
||||
// We need to increase the width
|
||||
width = this._origWidth + (newPageX - this._origPageX);
|
||||
}
|
||||
|
||||
if (width > 100) {
|
||||
this.setState({
|
||||
width: width
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function hexToRgb(hex: string, opacity: number) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
const rgb = result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
|
||||
if (rgb) {
|
||||
const {
|
||||
r,
|
||||
g,
|
||||
b
|
||||
} = rgb;
|
||||
return `rgba(${r},${g},${b}, ${opacity})`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import "./WorkItemRenderer.scss";
|
||||
|
||||
import * as React from 'react';
|
||||
import { IDimension } from '../../../redux/types';
|
||||
import { getRowColumnStyle } from '../gridhelper';
|
||||
|
||||
export class WorkItemShadow extends React.Component<IDimension, {}> {
|
||||
public render() {
|
||||
const style = getRowColumnStyle(this.props);
|
||||
return (
|
||||
<div className="work-item-shadow" style={style}>
|
||||
<div className="title">x</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { IDimension } from "../../redux/types";
|
||||
|
||||
export function getRowColumnStyle(dimension: IDimension) {
|
||||
return getStyle(dimension.startRow, dimension.endRow, dimension.startCol, dimension.endCol);
|
||||
}
|
||||
|
||||
|
||||
export function getStyle(startRow, endRow, startCol, endCol) {
|
||||
return {
|
||||
'grid-column': `${startCol} / ${endCol}`,
|
||||
'grid-row': `${startRow} / ${endRow}`,
|
||||
'-ms-grid-row': `${startRow}`,
|
||||
'-ms-grid-row-span': `${endRow - startRow}`,
|
||||
'-ms-grid-column': `${startCol}`,
|
||||
'-ms-grid-column-span': `${endCol - startCol}`
|
||||
}
|
||||
}
|
||||
|
||||
export function getTemplateColumns(fixedColumns: string[], count: number, size: string) {
|
||||
let str = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
str = str + size + ' ';
|
||||
}
|
||||
|
||||
const fixedColumnsStr = fixedColumns.join(' ');
|
||||
|
||||
return {
|
||||
'grid-template-columns': `${fixedColumnsStr} repeat(${count}, ${size})`,
|
||||
'-ms-grid-columns': `${fixedColumnsStr} ${str}`
|
||||
};
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { createStore, applyMiddleware, Store, compose } from 'redux';
|
||||
import { reducers, IFeatureTimelineRawState } from './store/index';
|
||||
import createSagaMiddleware from 'redux-saga'
|
||||
import { trackActions } from './sagas/trackActions';
|
||||
import { watchSagaActions } from './sagas';
|
||||
|
||||
export default function configureStore(
|
||||
initialState: IFeatureTimelineRawState
|
||||
): Store<IFeatureTimelineRawState> {
|
||||
|
||||
const sagaMiddleWare = createSagaMiddleware();
|
||||
const middleware = applyMiddleware(sagaMiddleWare, trackActions);
|
||||
|
||||
// Setup for using the redux dev tools in chrome
|
||||
// https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
|
||||
const composeEnhancers = window["__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"] || compose;
|
||||
|
||||
const store = createStore<IFeatureTimelineRawState>(
|
||||
reducers,
|
||||
initialState,
|
||||
composeEnhancers(middleware));
|
||||
|
||||
sagaMiddleWare.run(watchSagaActions);
|
||||
return store;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { TeamSettingsIteration } from "TFS/Work/Contracts";
|
||||
|
||||
export function compareIteration(i1: TeamSettingsIteration, i2: TeamSettingsIteration) : number {
|
||||
if (hasDates(i1) && !hasDates(i2)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (hasDates(i2) && !hasDates(i1)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!hasDates(i1) && !hasDates(i2)) {
|
||||
return comparePath(i1, i2);
|
||||
}
|
||||
|
||||
if (getStartTime(i1) === getStartTime(i2)) {
|
||||
return getFinishTime(i1) - getFinishTime(i2);
|
||||
}
|
||||
|
||||
return getStartTime(i1) - getStartTime(i2);
|
||||
}
|
||||
|
||||
|
||||
function hasDates(iteration: TeamSettingsIteration): boolean {
|
||||
return !!iteration.attributes.startDate && !!iteration.attributes.finishDate;
|
||||
}
|
||||
|
||||
function comparePath(i1: TeamSettingsIteration, i2: TeamSettingsIteration): number {
|
||||
return i1.path.localeCompare(i2.path);
|
||||
}
|
||||
|
||||
function getStartTime(iteration: TeamSettingsIteration): number {
|
||||
return iteration.attributes.startDate.getTime();
|
||||
}
|
||||
|
||||
function getFinishTime(iteration: TeamSettingsIteration): number {
|
||||
return iteration.attributes.finishDate.getTime();
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`test initialize saga basic test 1`] = `
|
||||
Object {
|
||||
"done": false,
|
||||
"value": Object {
|
||||
"@@redux-saga/IO": true,
|
||||
"PUT": Object {
|
||||
"action": Object {
|
||||
"payload": true,
|
||||
"type": "@@loading/loading",
|
||||
},
|
||||
"channel": null,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test initialize saga basic test 2`] = `
|
||||
Object {
|
||||
"done": false,
|
||||
"value": Object {
|
||||
"@@redux-saga/IO": true,
|
||||
"CALL": Object {
|
||||
"args": Array [
|
||||
Object {
|
||||
"payload": Object {
|
||||
"backlogLevelName": "backlogLevel",
|
||||
"projectId": "projectId",
|
||||
"teamId": "teamId",
|
||||
},
|
||||
"type": "@@common/initialize",
|
||||
},
|
||||
],
|
||||
"context": null,
|
||||
"fn": [Function],
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test initialize saga basic test 3`] = `
|
||||
Object {
|
||||
"done": false,
|
||||
"value": Object {
|
||||
"@@redux-saga/IO": true,
|
||||
"PUT": Object {
|
||||
"action": Object {
|
||||
"payload": false,
|
||||
"type": "@@loading/loading",
|
||||
},
|
||||
"channel": null,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test initialize saga basic test 4`] = `
|
||||
Object {
|
||||
"done": true,
|
||||
"value": undefined,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,21 @@
|
|||
declare var it, expect;
|
||||
|
||||
import { callinitialize } from '../initialize';
|
||||
import { createInitialize } from '../../store/common/actioncreators';
|
||||
|
||||
it('test initialize saga basic test', () => {
|
||||
const initializeAction = createInitialize("projectId", "teamId", "backlogLevel");
|
||||
const saga = callinitialize(initializeAction);
|
||||
|
||||
// Expect loading true
|
||||
expect(saga.next()).toMatchSnapshot();
|
||||
|
||||
// Expect call to initialize saga
|
||||
expect(saga.next()).toMatchSnapshot();
|
||||
|
||||
// Expect loading false
|
||||
expect(saga.next()).toMatchSnapshot();
|
||||
|
||||
// Expect null
|
||||
expect(saga.next()).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { takeEvery } from "redux-saga/effects";
|
||||
import { ClearOverrideIterationType, LaunchWorkItemFormActionType, StartUpdateWorkitemIterationActionType } from "../store/workItems/actions";
|
||||
import { launchWorkItemFormSaga } from "./launchWorkItemFormSaga";
|
||||
import { InitializeType } from "../store/common/actions";
|
||||
import { callinitialize } from "./initialize";
|
||||
import { launchOverrideWorkItemIteration, launchClearOverrideIteration, launchSaveOverrideIteration } from "./workItemOverrideIterationListner";
|
||||
import { OverrideIterationEndType, SaveOverrideIterationActionType } from "../store/overrideIterationProgress/actions";
|
||||
import { updateWorkItemIteration } from "./updateWorkItemIterationListner";
|
||||
|
||||
export function* watchSagaActions() {
|
||||
yield takeEvery(OverrideIterationEndType, launchOverrideWorkItemIteration);
|
||||
yield takeEvery(SaveOverrideIterationActionType, launchSaveOverrideIteration);
|
||||
yield takeEvery(ClearOverrideIterationType, launchClearOverrideIteration);
|
||||
yield takeEvery(LaunchWorkItemFormActionType, launchWorkItemFormSaga);
|
||||
yield takeEvery(InitializeType, callinitialize);
|
||||
yield takeEvery(StartUpdateWorkitemIterationActionType, updateWorkItemIteration);
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
import * as VSS_Service from 'VSS/Service';
|
||||
import {
|
||||
all,
|
||||
call,
|
||||
put
|
||||
} from 'redux-saga/effects';
|
||||
import { backlogConfigurationReceived } from '../store/backlogconfiguration/actionCreators';
|
||||
import { genericError } from '../store/error/actionCreators';
|
||||
import { WorkItemTrackingHttpClient } from 'TFS/WorkItemTracking/RestClient';
|
||||
import { WorkHttpClient } from 'TFS/Work/RestClient';
|
||||
import { InitializeAction } from '../store/common/actions';
|
||||
import { teamSettingsIterationReceived, changeDisplayIterationCount } from '../store/teamiterations/actionCreators';
|
||||
import { workItemLinksReceived, workItemsReceived, setOverrideIteration } from '../store/workitems/actionCreators';
|
||||
import { WorkItemMetadataService } from '../../Services/WorkItemMetadataService';
|
||||
import { workItemTypesReceived } from '../store/workitemmetadata/actionCreators';
|
||||
import TFS_Core_Contracts = require('TFS/Core/Contracts');
|
||||
import Contracts = require('TFS/Work/Contracts');
|
||||
import WitContracts = require('TFS/WorkItemTracking/Contracts');
|
||||
import { loading } from '../store/loading/actionCreators';
|
||||
import { IOverriddenIterationDuration } from '../store';
|
||||
|
||||
// For sagas read https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
|
||||
// For details saga effects read https://redux-saga.js.org/docs/basics/DeclarativeEffects.html
|
||||
|
||||
// Setup to call initialize saga for every initialize action
|
||||
|
||||
export function* callinitialize(action: InitializeAction) {
|
||||
yield put(loading(true));
|
||||
yield call(handleInitialize, action);
|
||||
yield put(loading(false));
|
||||
}
|
||||
|
||||
export function* handleInitialize(action: InitializeAction) {
|
||||
const {
|
||||
projectId,
|
||||
teamId
|
||||
} = action.payload;
|
||||
const teamContext = {
|
||||
teamId,
|
||||
projectId
|
||||
} as TFS_Core_Contracts.TeamContext;
|
||||
|
||||
const workHttpClient = VSS_Service.getClient(WorkHttpClient);
|
||||
const metadatService = WorkItemMetadataService.getInstance();
|
||||
const witHttpClient = VSS_Service.getClient(WorkItemTrackingHttpClient);
|
||||
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
|
||||
if (!workHttpClient.getBacklogConfigurations) {
|
||||
yield put(genericError("This extension is supported on Team Foundation Server 2018 or above."));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch backlog config, team iterations, workItem types and state metadata in parallel
|
||||
const [bc, tis, wits, overriddenWorkItemIterations, iterationDisplayOptions, ts, tfv] = yield all([
|
||||
call(workHttpClient.getBacklogConfigurations.bind(workHttpClient), teamContext),
|
||||
call(workHttpClient.getTeamIterations.bind(workHttpClient), teamContext),
|
||||
//call(metadatService.getStates.bind(metadatService), projectId),
|
||||
call(metadatService.getWorkItemTypes.bind(metadatService), projectId),
|
||||
call(dataService.getValue.bind(dataService), "overriddenWorkItemIterations"),
|
||||
call(dataService.getValue.bind(dataService), "iterationDisplayOptions"),
|
||||
call(workHttpClient.getTeamSettings.bind(workHttpClient), teamContext),
|
||||
call(workHttpClient.getTeamFieldValues.bind(workHttpClient), teamContext)
|
||||
]);
|
||||
|
||||
yield put(backlogConfigurationReceived(projectId, teamId, bc));
|
||||
yield put(teamSettingsIterationReceived(projectId, teamId, tis));
|
||||
if (iterationDisplayOptions) {
|
||||
yield put(changeDisplayIterationCount(iterationDisplayOptions.count, iterationDisplayOptions.projectId, iterationDisplayOptions.teamId));
|
||||
}
|
||||
yield put(workItemTypesReceived(projectId, wits));
|
||||
//yield put(workItemStateColorsReceived(projectId, stateColors));
|
||||
|
||||
const backlogConfig: Contracts.BacklogConfiguration = bc;
|
||||
const teamSettings: Contracts.TeamSetting = ts;
|
||||
const teamFieldValues: Contracts.TeamFieldValues = tfv;
|
||||
|
||||
// For now show only lowest level of portfolio backlog
|
||||
backlogConfig.portfolioBacklogs.sort((b1, b2) => b1.rank - b2.rank);
|
||||
const currentBacklogLevel = backlogConfig.portfolioBacklogs[0];
|
||||
const workItemTypes = currentBacklogLevel.workItemTypes.map(w => `'${w.name}'`).join(",");
|
||||
const stateInfo: Contracts.WorkItemTypeStateInfo[] = backlogConfig.workItemTypeMappedStates.filter(wtms => currentBacklogLevel.workItemTypes.some(wit => wit.name.toLowerCase() === wtms.workItemTypeName.toLowerCase()));
|
||||
const orderField = backlogConfig.backlogFields.typeFields["Order"];
|
||||
|
||||
let backlogIteration = teamSettings.backlogIteration.path || teamSettings.backlogIteration.name;
|
||||
if (backlogIteration[0] === "\\") {
|
||||
const webContext = VSS.getWebContext();
|
||||
backlogIteration = webContext.project.name + backlogIteration;
|
||||
}
|
||||
backlogIteration = _escape(backlogIteration);
|
||||
|
||||
const workItemTypeAndStatesClause =
|
||||
stateInfo
|
||||
.map(si => {
|
||||
const states = Object.keys(si.states).filter(state => si.states[state] === "InProgress")
|
||||
.map(state => _escape(state))
|
||||
.join("', '");
|
||||
|
||||
return `(
|
||||
[System.WorkItemType] = '${_escape(si.workItemTypeName)}'
|
||||
AND [System.State] IN ('${states}')
|
||||
)`;
|
||||
|
||||
}).join(" OR ");
|
||||
|
||||
const teamFieldClause = teamFieldValues.values.map((tfValue) => {
|
||||
const operator = tfValue.includeChildren ? "UNDER" : "=";
|
||||
return `[${_escape(teamFieldValues.field.referenceName)}] ${operator} '${_escape(tfValue.value)}'`;
|
||||
|
||||
}).join(" OR ");
|
||||
|
||||
const wiql = `SELECT System.Id
|
||||
FROM WorkItems
|
||||
WHERE [System.WorkItemType] IN (${workItemTypes})
|
||||
AND [System.IterationPath] UNDER '${backlogIteration}'
|
||||
AND (${workItemTypeAndStatesClause})
|
||||
AND (${teamFieldClause})
|
||||
ORDER BY [${orderField}] ASC,[System.Id] ASC`;
|
||||
|
||||
const queryResults: WitContracts.WorkItemQueryResult = yield call(witHttpClient.queryByWiql.bind(witHttpClient), { query: wiql }, projectId);
|
||||
|
||||
// Get work items for backlog level
|
||||
const backlogLevelWorkItemIds: number[] = [];
|
||||
let childWorkItemIds: number[] = [];
|
||||
let parentWorkItemIds: number[] = [];
|
||||
let workItemsToPage: number[] = [];
|
||||
|
||||
// Get child work items and page all work items
|
||||
if (queryResults && queryResults.workItems && queryResults.workItems.length > 0) {
|
||||
|
||||
const potentialBacklogLevelWorkItemIds = queryResults.workItems.map(w => w.id);
|
||||
|
||||
let pagedWorkItems = yield call(_pageWorkItemFields, potentialBacklogLevelWorkItemIds, [orderField]);
|
||||
|
||||
pagedWorkItems = pagedWorkItems.filter((wi) => _isInProgress(wi, bc));
|
||||
|
||||
const childBacklogLevel = yield call(_findChildBacklogLevel, currentBacklogLevel, bc);
|
||||
const parentBacklogLevel = yield call(_findParentBacklogLevel, currentBacklogLevel, bc);
|
||||
backlogLevelWorkItemIds.push(...pagedWorkItems.map((wi) => wi.id));
|
||||
|
||||
const childQueryResult: WitContracts.WorkItemQueryResult = yield call(_runChildWorkItemQuery, backlogLevelWorkItemIds, projectId, childBacklogLevel);
|
||||
if (childQueryResult && childQueryResult.workItemRelations) {
|
||||
childWorkItemIds = childQueryResult.workItemRelations
|
||||
.filter(link => link.target && link.rel)
|
||||
.map((link) => link.target.id);
|
||||
workItemsToPage.push(...childWorkItemIds);
|
||||
}
|
||||
|
||||
let parentLinks = [];
|
||||
if (parentBacklogLevel) {
|
||||
const parentQueryResult: WitContracts.WorkItemQueryResult = yield call(_runParentWorkItemQuery, backlogLevelWorkItemIds, projectId, parentBacklogLevel);
|
||||
parentLinks = parentQueryResult ? parentQueryResult.workItemRelations : [];
|
||||
}
|
||||
|
||||
parentWorkItemIds = parentLinks
|
||||
.filter(link => link.target && link.rel)
|
||||
.map((link) => link.target.id);
|
||||
workItemsToPage.push(...parentWorkItemIds);
|
||||
|
||||
const workItems: WitContracts.WorkItem[] = yield call(_pageWorkItemFields, workItemsToPage, [orderField]);
|
||||
workItems.push(...pagedWorkItems);
|
||||
workItems.sort((w1, w2) => w1.fields[orderField] - w2.fields[orderField]);
|
||||
|
||||
// Call action creators to update work items and links in the store
|
||||
yield put(workItemsReceived(workItems, parentWorkItemIds, backlogLevelWorkItemIds, childWorkItemIds));
|
||||
const linksReceived = childQueryResult ? childQueryResult.workItemRelations : [];
|
||||
linksReceived.push(...parentLinks);
|
||||
yield put(workItemLinksReceived(linksReceived));
|
||||
|
||||
|
||||
if (overriddenWorkItemIterations) {
|
||||
const currentValueTypes: IDictionaryNumberTo<IOverriddenIterationDuration> = JSON.parse(overriddenWorkItemIterations);
|
||||
|
||||
for (const key in currentValueTypes) {
|
||||
if (currentValueTypes.hasOwnProperty(key)) {
|
||||
const workItemId = Number(key);
|
||||
yield put(setOverrideIteration(
|
||||
workItemId,
|
||||
currentValueTypes[workItemId].startIterationId,
|
||||
currentValueTypes[workItemId].endIterationId,
|
||||
currentValueTypes[workItemId].user));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
yield put(genericError(error));
|
||||
}
|
||||
}
|
||||
|
||||
function _escape(value: string): string {
|
||||
return value.replace("'", "''");
|
||||
}
|
||||
|
||||
function _isInProgress(workItem: WitContracts.WorkItem, backlogConfig: Contracts.BacklogConfiguration) {
|
||||
return (backlogConfig.workItemTypeMappedStates.find((t) => t.workItemTypeName == workItem.fields["System.WorkItemType"]).states[workItem.fields["System.State"]] === "InProgress");
|
||||
}
|
||||
|
||||
function _findChildBacklogLevel(
|
||||
backlogLevel: Contracts.BacklogLevelConfiguration,
|
||||
backlogConfig: Contracts.BacklogConfiguration):
|
||||
Contracts.BacklogLevelConfiguration {
|
||||
let childBacklogLevel = backlogConfig.portfolioBacklogs.find((level) => level.rank < backlogLevel.rank);
|
||||
if (childBacklogLevel) {
|
||||
return childBacklogLevel;
|
||||
}
|
||||
return backlogConfig.requirementBacklog;
|
||||
}
|
||||
|
||||
function _findParentBacklogLevel(
|
||||
backlogLevel: Contracts.BacklogLevelConfiguration,
|
||||
backlogConfig: Contracts.BacklogConfiguration):
|
||||
Contracts.BacklogLevelConfiguration {
|
||||
|
||||
let parentBacklogLevel = backlogConfig
|
||||
.portfolioBacklogs
|
||||
.filter((level) => level.rank > backlogLevel.rank)
|
||||
.sort((b1, b2) => b1.rank - b2.rank)[0];
|
||||
|
||||
if (parentBacklogLevel) {
|
||||
return parentBacklogLevel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function _runChildWorkItemQuery(
|
||||
ids: number[],
|
||||
project: string,
|
||||
backlogLevel: Contracts.BacklogLevelConfiguration):
|
||||
Promise<WitContracts.WorkItemQueryResult> {
|
||||
if (!ids || ids.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const idClause = ids.join(",");
|
||||
const witClause = backlogLevel.workItemTypes.map(wit => "'" + wit.name + "'").join(",");
|
||||
const wiql =
|
||||
`SELECT [System.Id]
|
||||
FROM WorkItemLinks
|
||||
WHERE (Source.[System.TeamProject] = @project and Source.[System.Id] in (${idClause}))
|
||||
AND ([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward')
|
||||
AND (Target.[System.TeamProject] = @project and Target.[System.WorkItemType] in (${witClause}))
|
||||
MODE (MayContain)`;
|
||||
const witHttpClient = VSS_Service.getClient(WorkItemTrackingHttpClient);
|
||||
return witHttpClient.queryByWiql({ query: wiql }, project);
|
||||
}
|
||||
|
||||
async function _runParentWorkItemQuery(
|
||||
ids: number[],
|
||||
project: string,
|
||||
backlogLevel: Contracts.BacklogLevelConfiguration):
|
||||
Promise<WitContracts.WorkItemQueryResult> {
|
||||
if (!ids || ids.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const idClause = ids.join(",");
|
||||
const witClause = backlogLevel.workItemTypes.map(wit => "'" + wit.name + "'").join(",");
|
||||
const wiql =
|
||||
`SELECT [System.Id]
|
||||
FROM WorkItemLinks
|
||||
WHERE (Source.[System.TeamProject] = @project and Source.[System.Id] in (${idClause}))
|
||||
AND ([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Reverse')
|
||||
AND (Target.[System.TeamProject] = @project and Target.[System.WorkItemType] in (${witClause}))
|
||||
MODE (MayContain)`;
|
||||
const witHttpClient = VSS_Service.getClient(WorkItemTrackingHttpClient);
|
||||
return witHttpClient.queryByWiql({ query: wiql }, project);
|
||||
}
|
||||
|
||||
async function _pageWorkItemFields(
|
||||
ids: number[],
|
||||
fields: string[]): Promise<WitContracts.WorkItem[]> {
|
||||
if (!ids || ids.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const commonFields = [
|
||||
"System.Id",
|
||||
"System.Title",
|
||||
"System.State",
|
||||
"System.WorkItemType",
|
||||
"System.IterationPath"
|
||||
];
|
||||
commonFields.push(...fields);
|
||||
const witHttpClient = VSS_Service.getClient(WorkItemTrackingHttpClient);
|
||||
return witHttpClient.getWorkItems(ids, commonFields);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { LaunchWorkItemFormAction } from "../store/workItems/actions";
|
||||
import { put, call } from "redux-saga/effects";
|
||||
import { WorkItemFormNavigationService, IWorkItemFormNavigationService } from "TFS/WorkItemTracking/Services";
|
||||
import { createInitialize } from "../store/common/actioncreators";
|
||||
import { getProjectId, getTeamId, getBacklogLevel } from "../selectors";
|
||||
|
||||
|
||||
export function* launchWorkItemFormSaga(action: LaunchWorkItemFormAction) {
|
||||
const workItemNavSvc: IWorkItemFormNavigationService = yield call(WorkItemFormNavigationService.getService);
|
||||
yield call(workItemNavSvc.openWorkItem.bind(workItemNavSvc), action.payload.workItemId);
|
||||
|
||||
// TODO: At this point the workitem returned after the update does not have
|
||||
// updated links so we do not have a way to identify if any of the links changed
|
||||
// our best bet is to update the workitems and relations by reinitializing
|
||||
const projectId = getProjectId();
|
||||
const teamId = getTeamId();
|
||||
const backlogLevel = getBacklogLevel();
|
||||
|
||||
const initializeAction = createInitialize(projectId, teamId, backlogLevel);
|
||||
yield put(initializeAction);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Middleware } from "redux";
|
||||
|
||||
export const trackActions: Middleware = api => next => action => {
|
||||
//if (action["track"])
|
||||
{
|
||||
// TODO: Publish telemetry
|
||||
// console.log("TELEMETRY: ", action);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import * as VSS_Service from 'VSS/Service';
|
||||
import { StartUpdateWorkitemIterationAction } from "../store/workItems/actions";
|
||||
import { put, call } from "redux-saga/effects";
|
||||
import { WorkItemTrackingHttpClient3_2 } from 'TFS/WorkItemTracking/RestClient';
|
||||
import { JsonPatchDocument } from 'VSS/WebApi/Contracts';
|
||||
import { workItemSaved, workItemSaveFailed, clearOverrideIteration } from '../store/workitems/actionCreators';
|
||||
import { saveOverrideIteration } from '../store/overrideIterationProgress/actionCreators';
|
||||
import { IWorkItemOverrideIteration } from '../store';
|
||||
|
||||
|
||||
export function* updateWorkItemIteration(action: StartUpdateWorkitemIterationAction) {
|
||||
const witHttpClient = VSS_Service.getClient(WorkItemTrackingHttpClient3_2);
|
||||
const {
|
||||
payload
|
||||
} = action;
|
||||
try {
|
||||
const doc: JsonPatchDocument = [{
|
||||
"op": "add",
|
||||
"path": "/fields/System.IterationPath",
|
||||
"value": payload.teamIteration.path
|
||||
}];
|
||||
|
||||
if (payload.override) {
|
||||
const overridePayload: IWorkItemOverrideIteration = {
|
||||
workItemId: payload.workItem,
|
||||
iterationDuration: {
|
||||
startIterationId: payload.teamIteration.id,
|
||||
endIterationId: payload.teamIteration.id,
|
||||
user: VSS.getWebContext().user.uniqueName
|
||||
},
|
||||
changingStart: false
|
||||
};
|
||||
yield put(saveOverrideIteration(overridePayload));
|
||||
} else {
|
||||
// Clear override iteration if any
|
||||
yield put(clearOverrideIteration(payload.workItem));
|
||||
}
|
||||
|
||||
// Update work item Iteration path
|
||||
yield call(witHttpClient.updateWorkItem.bind(witHttpClient), doc, action.payload.workItem);
|
||||
yield put(workItemSaved([action.payload.workItem]));
|
||||
}
|
||||
catch (error) {
|
||||
yield put(workItemSaveFailed([action.payload.workItem], error));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { ClearOverrideIterationAction } from "../store/workItems/actions";
|
||||
import { put, call, select } from "redux-saga/effects";
|
||||
import { workItemOverrideIterationSelector } from "../selectors";
|
||||
import { IWorkItemOverrideIteration } from "../store";
|
||||
import { setOverrideIteration } from "../store/workitems/actionCreators";
|
||||
import { cleanupOverrideIteration, saveOverrideIteration } from "../store/overrideIterationProgress/actionCreators";
|
||||
import { OverrideIterationEndAction, SaveOverrideIterationAction } from "../store/overrideIterationProgress/actions";
|
||||
|
||||
|
||||
export function* launchOverrideWorkItemIteration(action: OverrideIterationEndAction) {
|
||||
const overrideIterationState: IWorkItemOverrideIteration = yield select(workItemOverrideIterationSelector());
|
||||
|
||||
if (!overrideIterationState) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(cleanupOverrideIteration());
|
||||
yield put(saveOverrideIteration(overrideIterationState));
|
||||
}
|
||||
|
||||
export function* launchSaveOverrideIteration(action: SaveOverrideIterationAction) {
|
||||
|
||||
const overrideIterationState = action.payload;
|
||||
|
||||
yield put(setOverrideIteration(overrideIterationState.workItemId, overrideIterationState.iterationDuration.startIterationId, overrideIterationState.iterationDuration.endIterationId, overrideIterationState.iterationDuration.user));
|
||||
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
|
||||
let currentValues = yield call(dataService.getValue.bind(dataService), "overriddenWorkItemIterations");
|
||||
if (currentValues) {
|
||||
currentValues = JSON.parse(currentValues);
|
||||
} else {
|
||||
currentValues = {};
|
||||
}
|
||||
currentValues[overrideIterationState.workItemId] = {
|
||||
startIterationId: overrideIterationState.iterationDuration.startIterationId,
|
||||
endIterationId: overrideIterationState.iterationDuration.endIterationId,
|
||||
user: overrideIterationState.iterationDuration.user
|
||||
};
|
||||
|
||||
yield call(dataService.setValue.bind(dataService), "overriddenWorkItemIterations", JSON.stringify(currentValues));
|
||||
}
|
||||
|
||||
export function* launchClearOverrideIteration(action: ClearOverrideIterationAction) {
|
||||
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
|
||||
let currentValues = yield call(dataService.getValue.bind(dataService), "overriddenWorkItemIterations");
|
||||
if (currentValues) {
|
||||
currentValues = JSON.parse(currentValues);
|
||||
} else {
|
||||
currentValues = {};
|
||||
}
|
||||
delete currentValues[action.payload];
|
||||
|
||||
yield call(dataService.setValue.bind(dataService), "overriddenWorkItemIterations", JSON.stringify(currentValues));
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
import { IWorkItemOverrideIteration } from "../store";
|
||||
import { IWorkItemHierarchy } from "./workItemHierarchySelector";
|
||||
import { TeamSettingsIteration } from "TFS/Work/Contracts";
|
||||
import { UIStatus, IDimension, CropWorkItem } from "../types";
|
||||
import { compareIteration } from "../helpers/iterationComparer";
|
||||
import { IIterationDisplayOptions } from "../store/teamiterations/types";
|
||||
|
||||
export interface IGridIteration {
|
||||
teamIteration: TeamSettingsIteration;
|
||||
dimension: IDimension;
|
||||
}
|
||||
|
||||
export interface IGridWorkItem {
|
||||
dimension: IDimension;
|
||||
workItem: IWorkItemHierarchy;
|
||||
isGap?: boolean;
|
||||
crop: CropWorkItem;
|
||||
gapColor?: string;
|
||||
}
|
||||
|
||||
export interface IGridView {
|
||||
emptyHeaderRow: IDimension[]; //Set of empty elements to place items on top of iteration header
|
||||
iterationHeader: IGridIteration[];
|
||||
iterationShadow: IGridIteration[];
|
||||
workItems: IGridWorkItem[];
|
||||
isSubGrid: boolean;
|
||||
workItemShadow: number;
|
||||
hideParents: boolean;
|
||||
iterationDisplayOptions: IIterationDisplayOptions;
|
||||
}
|
||||
|
||||
export function getGridView(
|
||||
uiStatus: UIStatus,
|
||||
teamIterations: TeamSettingsIteration[],
|
||||
workItems: IWorkItemHierarchy[],
|
||||
workItemOverrideIteration: IWorkItemOverrideIteration,
|
||||
iterationDisplayOptions: IIterationDisplayOptions = null,
|
||||
isSubGrid: boolean = false
|
||||
): IGridView {
|
||||
|
||||
if (uiStatus !== UIStatus.Default) {
|
||||
return {
|
||||
emptyHeaderRow: [],
|
||||
iterationHeader: [],
|
||||
iterationShadow: [],
|
||||
workItems: [],
|
||||
isSubGrid,
|
||||
workItemShadow: 0,
|
||||
hideParents: false,
|
||||
iterationDisplayOptions: null
|
||||
}
|
||||
}
|
||||
|
||||
const hideParents = isSubGrid || (workItems.length === 1 && workItems[0].id === 0);
|
||||
const displayIterations = getDisplayIterations(teamIterations, workItems, iterationDisplayOptions);
|
||||
const gridWorkItems = getGridWorkItems(
|
||||
teamIterations,
|
||||
displayIterations,
|
||||
iterationDisplayOptions,
|
||||
workItems,
|
||||
/* startRow */ 3,
|
||||
/* startCol */ 1,
|
||||
hideParents);
|
||||
|
||||
let workItemShadow = 0;
|
||||
if (workItemOverrideIteration && workItemOverrideIteration.workItemId) {
|
||||
workItemShadow = workItemOverrideIteration.workItemId;
|
||||
}
|
||||
|
||||
const view: IGridView = {
|
||||
emptyHeaderRow: [],
|
||||
iterationHeader: [],
|
||||
iterationShadow: [],
|
||||
workItems: gridWorkItems,
|
||||
isSubGrid,
|
||||
workItemShadow,
|
||||
hideParents,
|
||||
iterationDisplayOptions
|
||||
};
|
||||
|
||||
// Calculate shadow and header
|
||||
const startRow = 1;
|
||||
const endRow = 2;
|
||||
const lastWorkItemRow = gridWorkItems.length > 0 ? gridWorkItems[gridWorkItems.length - 1].dimension.endRow + 1 : endRow + 1;
|
||||
let startCol = hideParents ? 1 : 2; // First column is for the epic
|
||||
displayIterations.forEach(teamIteration => {
|
||||
|
||||
const endCol = startCol + 1;
|
||||
const emptyRowDimension: IDimension = {
|
||||
startCol,
|
||||
startRow,
|
||||
endRow,
|
||||
endCol
|
||||
};
|
||||
|
||||
view.emptyHeaderRow.push(emptyRowDimension);
|
||||
|
||||
const dimension: IDimension = {
|
||||
startCol,
|
||||
startRow: startRow + 1,
|
||||
endRow,
|
||||
endCol
|
||||
};
|
||||
|
||||
const gridIteration: IGridIteration = {
|
||||
teamIteration,
|
||||
dimension
|
||||
};
|
||||
|
||||
view.iterationHeader.push(gridIteration);
|
||||
|
||||
const shadowDimension: IDimension = {
|
||||
startRow: startRow + 2,
|
||||
startCol,
|
||||
endCol,
|
||||
endRow: lastWorkItemRow
|
||||
};
|
||||
const shadowIteration: IGridIteration = {
|
||||
teamIteration,
|
||||
dimension: shadowDimension
|
||||
}
|
||||
view.iterationShadow.push(shadowIteration);
|
||||
startCol++;
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
export function getDisplayIterations(
|
||||
teamIterations: TeamSettingsIteration[],
|
||||
workItems: IWorkItemHierarchy[],
|
||||
iterationDisplayOptions?: IIterationDisplayOptions): TeamSettingsIteration[] {
|
||||
|
||||
// Sort the input iteration
|
||||
teamIterations = teamIterations.slice().sort(compareIteration);
|
||||
|
||||
if (iterationDisplayOptions) {
|
||||
return teamIterations.slice(iterationDisplayOptions.startIndex, iterationDisplayOptions.endIndex + 1);
|
||||
}
|
||||
|
||||
let firstIteration: TeamSettingsIteration = null;
|
||||
let lastIteration: TeamSettingsIteration = null;
|
||||
|
||||
// Get all iterations that come in the range of the workItems
|
||||
const calcFirstLastIteration = (workItem: IWorkItemHierarchy) => {
|
||||
if (firstIteration === null) {
|
||||
firstIteration = workItem.iterationDuration.startIteration;
|
||||
lastIteration = workItem.iterationDuration.endIteration;
|
||||
} else {
|
||||
if (compareIteration(workItem.iterationDuration.startIteration, firstIteration) < 0) {
|
||||
firstIteration = workItem.iterationDuration.startIteration;
|
||||
}
|
||||
|
||||
if (compareIteration(workItem.iterationDuration.endIteration, lastIteration) > 0) {
|
||||
lastIteration = workItem.iterationDuration.endIteration;
|
||||
}
|
||||
}
|
||||
|
||||
workItem.children.forEach(child => calcFirstLastIteration(child));
|
||||
};
|
||||
|
||||
workItems.forEach(child => calcFirstLastIteration(child));
|
||||
|
||||
// Get two to the left and two to the right iterations from teamIterations
|
||||
let startIndex = teamIterations.findIndex(i => i.id === firstIteration.id) - 2;
|
||||
let endIndex = teamIterations.findIndex(i => i.id === lastIteration.id) + 2;
|
||||
|
||||
startIndex = startIndex < 0 ? 0 : startIndex;
|
||||
endIndex = endIndex >= teamIterations.length ? teamIterations.length - 1 : endIndex;
|
||||
|
||||
return teamIterations.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
function workItemCompare(w1: IWorkItemHierarchy, w2: IWorkItemHierarchy) {
|
||||
if (w1.order === w2.order) {
|
||||
return w1.id - w2.id;
|
||||
}
|
||||
|
||||
return w1.order - w2.order;
|
||||
}
|
||||
|
||||
export function getGridWorkItems(
|
||||
teamIterations: TeamSettingsIteration[],
|
||||
displayIterations: TeamSettingsIteration[],
|
||||
iterationDisplayOptions: IIterationDisplayOptions,
|
||||
workItems: IWorkItemHierarchy[],
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
hideParents: boolean): IGridWorkItem[] {
|
||||
|
||||
const output: IGridWorkItem[] = [];
|
||||
workItems = workItems.sort(workItemCompare);
|
||||
|
||||
let lastColumn = displayIterations.length + 1;
|
||||
if (!hideParents) {
|
||||
lastColumn++;
|
||||
}
|
||||
|
||||
workItems.forEach((parent, index) => {
|
||||
const parentStartRow = startRow;
|
||||
const parentStartColumn = startColumn;
|
||||
let parentEndColumn = parentStartColumn;
|
||||
|
||||
const children = parent.children.sort(workItemCompare);
|
||||
const parentEndRow = parentStartRow + parent.children.length + (children.length > 0 ? 1 : 0); // Add additional row for just empty workitem to show gap between
|
||||
|
||||
if (!hideParents) {
|
||||
parentEndColumn = parentStartColumn + 1;
|
||||
|
||||
const dimension: IDimension = {
|
||||
startRow: parentStartRow,
|
||||
startCol: parentStartColumn,
|
||||
endRow: parentEndRow,
|
||||
endCol: parentEndColumn
|
||||
};
|
||||
|
||||
const gridItem: IGridWorkItem = { workItem: parent, dimension, crop: CropWorkItem.None };
|
||||
output.push(gridItem);
|
||||
|
||||
}
|
||||
|
||||
let childStartRow = parentStartRow;
|
||||
const allIterations = iterationDisplayOptions ? teamIterations : displayIterations;
|
||||
|
||||
children.forEach(child => {
|
||||
const childEndRow = childStartRow + 1;
|
||||
let startIterationIndex = allIterations.findIndex(gi => gi.id === child.iterationDuration.startIteration.id);
|
||||
let endIterationIndex = allIterations.findIndex(gi => gi.id === child.iterationDuration.endIteration.id);
|
||||
|
||||
let crop: CropWorkItem = CropWorkItem.None;
|
||||
let outofScope = false;
|
||||
|
||||
// Either drop of set out of scope if the child item iteration is out of scope
|
||||
if (iterationDisplayOptions) {
|
||||
|
||||
if (startIterationIndex > iterationDisplayOptions.endIndex || endIterationIndex < iterationDisplayOptions.startIndex) {
|
||||
outofScope = true;
|
||||
}
|
||||
|
||||
if (iterationDisplayOptions.startIndex > startIterationIndex) {
|
||||
startIterationIndex = 0;
|
||||
crop = CropWorkItem.Left;
|
||||
} else {
|
||||
startIterationIndex = displayIterations.findIndex(gi => gi.id === child.iterationDuration.startIteration.id);
|
||||
}
|
||||
|
||||
if (endIterationIndex > iterationDisplayOptions.endIndex) {
|
||||
endIterationIndex = displayIterations.length - 1;
|
||||
crop = crop === CropWorkItem.Left ? CropWorkItem.Both : CropWorkItem.Right;
|
||||
} else {
|
||||
endIterationIndex = displayIterations.findIndex(gi => gi.id === child.iterationDuration.endIteration.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!outofScope) {
|
||||
const childStartColumn = parentEndColumn + startIterationIndex;
|
||||
const childEndColumn = parentEndColumn + endIterationIndex + 1;
|
||||
|
||||
const dimension: IDimension = {
|
||||
startRow: childStartRow,
|
||||
startCol: childStartColumn,
|
||||
endRow: childEndRow,
|
||||
endCol: childEndColumn
|
||||
};
|
||||
|
||||
const gridItem: IGridWorkItem = { workItem: child, dimension, crop };
|
||||
output.push(gridItem);
|
||||
|
||||
childStartRow++;
|
||||
}
|
||||
});
|
||||
|
||||
if (children.length > 0 && index < (workItems.length - 1)) {
|
||||
|
||||
output.push({
|
||||
workItem: <IWorkItemHierarchy>{
|
||||
id: -1,
|
||||
title: "",
|
||||
children: [],
|
||||
shouldShowDetails: false
|
||||
},
|
||||
dimension: {
|
||||
startRow: parentEndRow - 1,
|
||||
endRow: parentEndRow,
|
||||
startCol: hideParents ? 1 : 2,
|
||||
endCol: lastColumn
|
||||
},
|
||||
isGap: true,
|
||||
gapColor: parent.color,
|
||||
crop: CropWorkItem.None
|
||||
});
|
||||
}
|
||||
|
||||
startRow = parentEndRow;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { IContributionContext } from "../store/common/types";
|
||||
import { createSelector } from "reselect";
|
||||
import { getWorkItemsForLevel } from "./workItemsForLevel";
|
||||
import { getUIStatus } from "./uistatus";
|
||||
import { IFeatureTimelineRawState } from "../store";
|
||||
import { WorkItemLevel } from "../store/workitems/types";
|
||||
import { getWorkItemHierarchy } from "./workItemHierarchySelector";
|
||||
import { getGridView } from "./gridViewSelector";
|
||||
import { getTeamIterations } from "./teamIterations";
|
||||
|
||||
export const getRawState = (state: IFeatureTimelineRawState) => state;
|
||||
export const getProjectId = () => {
|
||||
const webContext = VSS.getWebContext();
|
||||
return webContext.project.id;
|
||||
}
|
||||
export const getTeamId = () => {
|
||||
const contributionContext: IContributionContext = VSS.getConfiguration();
|
||||
if (contributionContext.team) {
|
||||
return contributionContext.team.id;
|
||||
}
|
||||
const webContext = VSS.getWebContext();
|
||||
return webContext.team.id;
|
||||
};
|
||||
|
||||
export const getBacklogLevel = () => {
|
||||
const contributionContext: IContributionContext = VSS.getConfiguration();
|
||||
return contributionContext.level;
|
||||
};
|
||||
|
||||
|
||||
export const iterationDisplayOptionsSelector = () => {
|
||||
return createSelector(
|
||||
[getRawState],
|
||||
(state) => {
|
||||
if (!state || !state.iterationState) {
|
||||
return null;
|
||||
}
|
||||
return state.iterationState.iterationDisplayOptions;
|
||||
});
|
||||
}
|
||||
|
||||
export const workItemIdsSelector = (level: WorkItemLevel) => {
|
||||
return createSelector(
|
||||
[getProjectId, getTeamId, getRawState],
|
||||
(projectId, teamId, state) => {
|
||||
if (!state || !state.workItemsState || !state.workItemsState.workItemInfos) {
|
||||
return [];
|
||||
}
|
||||
return getWorkItemsForLevel(state.workItemsState.workItemInfos, level);
|
||||
});
|
||||
}
|
||||
|
||||
export const workItemOverrideIterationSelector = () => {
|
||||
return createSelector([getRawState], (state) => state.workItemOverrideIteration);
|
||||
}
|
||||
|
||||
export const uiStatusSelector = () => {
|
||||
return createSelector([getProjectId, getTeamId, getRawState], getUIStatus);
|
||||
}
|
||||
|
||||
export const workItemHierarchySelector = () => {
|
||||
return createSelector([getProjectId, getTeamId, uiStatusSelector(), getRawState], getWorkItemHierarchy);
|
||||
};
|
||||
|
||||
export const teamIterationsSelector = () => {
|
||||
return createSelector([getProjectId, getTeamId, uiStatusSelector(), getRawState], getTeamIterations);
|
||||
}
|
||||
|
||||
export const gridViewSelector = () => {
|
||||
return createSelector([
|
||||
uiStatusSelector(),
|
||||
teamIterationsSelector(),
|
||||
workItemHierarchySelector(),
|
||||
workItemOverrideIterationSelector(),
|
||||
iterationDisplayOptionsSelector()
|
||||
],
|
||||
getGridView)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { IFeatureTimelineRawState } from "../store";
|
||||
import { TeamSettingsIteration } from "TFS/Work/Contracts";
|
||||
import { UIStatus } from "../types";
|
||||
|
||||
export function getTeamIterations(
|
||||
projectId: string,
|
||||
teamId: string,
|
||||
uiStatus: UIStatus,
|
||||
rawState: IFeatureTimelineRawState): TeamSettingsIteration[] {
|
||||
|
||||
if (uiStatus !== UIStatus.Default) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rawState.iterationState.teamSettingsIterations[projectId][teamId];
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { IFeatureTimelineRawState } from "../store";
|
||||
import { UIStatus } from "../types";
|
||||
import { compareIteration } from "../helpers/iterationComparer";
|
||||
|
||||
export function getUIStatus(
|
||||
projectId: string,
|
||||
teamId: string,
|
||||
rawState: IFeatureTimelineRawState): UIStatus {
|
||||
const {
|
||||
iterationState
|
||||
} = rawState;
|
||||
|
||||
if (rawState.error) {
|
||||
return UIStatus.Error;
|
||||
}
|
||||
|
||||
if (rawState.loading) {
|
||||
return UIStatus.Loading;
|
||||
}
|
||||
|
||||
if (!iterationState ||
|
||||
!iterationState.teamSettingsIterations ||
|
||||
!iterationState.teamSettingsIterations[projectId] ||
|
||||
!iterationState.teamSettingsIterations[projectId][teamId] ||
|
||||
iterationState.teamSettingsIterations[projectId][teamId].length === 0) {
|
||||
return UIStatus.NoTeamIterations;
|
||||
}
|
||||
|
||||
const iterations = iterationState.teamSettingsIterations[projectId][teamId].slice();
|
||||
|
||||
iterations.sort(compareIteration);
|
||||
|
||||
if (Object.keys(rawState.workItemsState.workItemInfos).length === 0) {
|
||||
return UIStatus.NoWorkItems;
|
||||
}
|
||||
|
||||
|
||||
return UIStatus.Default;
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
import { getWorkItemsForLevel } from './workItemsForLevel';
|
||||
import { IFeatureTimelineRawState, IIterationDuration, IterationDurationKind } from '../store';
|
||||
import { IWorkItemInfo, WorkItemLevel } from '../store/workitems/types';
|
||||
import { TeamSettingsIteration, TimeFrame } from 'TFS/Work/Contracts';
|
||||
import { WorkItem } from 'TFS/WorkItemTracking/Contracts';
|
||||
import { UIStatus } from '../types';
|
||||
import { compareIteration } from '../helpers/iterationComparer';
|
||||
|
||||
export interface IWorkItemHierarchy {
|
||||
id: number;
|
||||
title: string;
|
||||
color: string;
|
||||
isRoot: boolean;
|
||||
workItem: WorkItem;
|
||||
order: number;
|
||||
iterationDuration: IIterationDuration;
|
||||
children: IWorkItemHierarchy[];
|
||||
shouldShowDetails: boolean;
|
||||
}
|
||||
|
||||
export function getWorkItemHierarchy(
|
||||
projectId: string,
|
||||
teamId: string,
|
||||
uiStatus: UIStatus,
|
||||
input: IFeatureTimelineRawState): IWorkItemHierarchy[] {
|
||||
|
||||
if (uiStatus !== UIStatus.Default) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {
|
||||
workItemsState
|
||||
} = input;
|
||||
|
||||
// Fetch work items at parent level
|
||||
const epicIds = getWorkItemsForLevel(workItemsState.workItemInfos, WorkItemLevel.Parent);
|
||||
|
||||
// Add unparent level as parent
|
||||
epicIds.unshift(0);
|
||||
|
||||
return getWorkItemsDetails(projectId, teamId, epicIds, input, /* isRoot */ true);
|
||||
}
|
||||
|
||||
function getWorkItemsDetails(projectId: string, teamId: string, ids: number[], input: IFeatureTimelineRawState, isRoot: boolean): IWorkItemHierarchy[] {
|
||||
return ids.map(id => getWorkItemDetails(projectId, teamId, id, input, isRoot));
|
||||
}
|
||||
|
||||
function getWorkItemDetails(projectId: string, teamId: string, id: number, input: IFeatureTimelineRawState, isRoot: boolean): IWorkItemHierarchy {
|
||||
const {
|
||||
workItemsState,
|
||||
workItemMetadata
|
||||
} = input;
|
||||
|
||||
|
||||
const workItem = id && workItemsState.workItemInfos[id].workItem;
|
||||
let workItemType = null;
|
||||
|
||||
if (workItem) {
|
||||
const workItemTypeName = workItem.fields["System.WorkItemType"];
|
||||
workItemType = workItemMetadata.metadata[projectId].workItemTypes.filter((wit) => wit.name.toLowerCase() === workItemTypeName.toLowerCase())[0];
|
||||
}
|
||||
|
||||
const children = getWorkItemsDetails(projectId, teamId, getChildrenId(workItemsState.workItemInfos, id), input, /* isRoot */ false);
|
||||
|
||||
// try getting start/end iteration from children
|
||||
let iterationDuration = getIterationDurationFromChildren(children);
|
||||
const iterations = getIterations(projectId, teamId, input);
|
||||
|
||||
// TODO: We will use the dropdown option value when available to toggle use overridden value or not
|
||||
// if the start/end iteration is overridden use that value
|
||||
if (input.savedOverriddenWorkItemIterations &&
|
||||
input.savedOverriddenWorkItemIterations[id]) {
|
||||
const si = input.savedOverriddenWorkItemIterations[id].startIterationId;
|
||||
const ei = input.savedOverriddenWorkItemIterations[id].endIterationId;
|
||||
const overridedBy = input.savedOverriddenWorkItemIterations[id].user;
|
||||
|
||||
const startIteration = iterations.find(i => i.id === si);
|
||||
const endIteration = iterations.find(i => i.id === ei);
|
||||
if (startIteration && endIteration) {
|
||||
iterationDuration = { startIteration, endIteration, kind: IterationDurationKind.UserOverridden, overridedBy };
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// if null use workItems start/end iteration
|
||||
if (workItem && (!iterationDuration.startIteration || !iterationDuration.endIteration)) {
|
||||
const iterationPath = workItem.fields["System.IterationPath"];
|
||||
const iteration = iterations.find((i) => i.path === iterationPath);
|
||||
|
||||
iterationDuration.startIteration = iteration;
|
||||
iterationDuration.endIteration = iteration;
|
||||
}
|
||||
|
||||
// If still null take currentIteration
|
||||
const currentIteration = getCurrentIteration(projectId, teamId, input);
|
||||
if (!iterationDuration.startIteration || !iterationDuration.endIteration) {
|
||||
iterationDuration.startIteration = currentIteration;
|
||||
iterationDuration.endIteration = currentIteration;
|
||||
iterationDuration.kind = IterationDurationKind.CurrentIteration;
|
||||
}
|
||||
|
||||
const orderFieldName = input.backlogConfiguration.backlogConfigurations[projectId][teamId].backlogFields.typeFields["Order"];
|
||||
const color = workItemType ? "#" + (workItemType.color.length > 6 ? workItemType.color.substr(2) : workItemType.color) : "#c2c8d1";
|
||||
const workItemDetails = {
|
||||
id,
|
||||
title: workItem ? workItem.fields["System.Title"] : "Unparented",
|
||||
color,
|
||||
order: workItem ? workItem.fields[orderFieldName] : 0,
|
||||
workItem,
|
||||
iterationDuration,
|
||||
children,
|
||||
isRoot,
|
||||
shouldShowDetails: !isRoot && (iterationDuration.kind === IterationDurationKind.ChildRollup || iterationDuration.kind === IterationDurationKind.UserOverridden)
|
||||
};
|
||||
|
||||
return workItemDetails;
|
||||
}
|
||||
|
||||
function getIterationDurationFromChildren(children: IWorkItemHierarchy[]): IIterationDuration {
|
||||
return children.reduce((prev, child) => {
|
||||
let {
|
||||
startIteration,
|
||||
endIteration
|
||||
} = prev;
|
||||
|
||||
if (!startIteration || !endIteration) {
|
||||
startIteration = child.iterationDuration.startIteration;
|
||||
endIteration = child.iterationDuration.endIteration;
|
||||
} else {
|
||||
if (compareIteration(child.iterationDuration.startIteration , startIteration) < 0) {
|
||||
startIteration = child.iterationDuration.startIteration;
|
||||
}
|
||||
|
||||
if (compareIteration(child.iterationDuration.endIteration, endIteration) > 0) {
|
||||
endIteration = child.iterationDuration.endIteration;
|
||||
}
|
||||
}
|
||||
return {
|
||||
startIteration,
|
||||
endIteration,
|
||||
kind: IterationDurationKind.ChildRollup
|
||||
}
|
||||
}, { startIteration: null, endIteration: null, kind: IterationDurationKind.None });
|
||||
}
|
||||
|
||||
function getCurrentIteration(projectId: string, teamId: string, input: IFeatureTimelineRawState): TeamSettingsIteration {
|
||||
const allIterations = getIterations(projectId, teamId, input);
|
||||
const currentIterations = allIterations.filter((itr) => TimeFrame && itr.attributes.timeFrame === TimeFrame.Current);
|
||||
if (currentIterations.length === 0) {
|
||||
return allIterations[0];
|
||||
}
|
||||
|
||||
|
||||
return currentIterations[0];
|
||||
}
|
||||
|
||||
function getIterations(projectId: string, teamId: string, input: IFeatureTimelineRawState) {
|
||||
if (!input ||
|
||||
!input.iterationState ||
|
||||
!input.iterationState.teamSettingsIterations ||
|
||||
!input.iterationState.teamSettingsIterations[projectId] ||
|
||||
!input.iterationState.teamSettingsIterations[projectId][teamId]) {
|
||||
|
||||
return [];
|
||||
}
|
||||
return input.iterationState.teamSettingsIterations[projectId][teamId];
|
||||
}
|
||||
|
||||
function getChildrenId(workItemInfos: IDictionaryNumberTo<IWorkItemInfo>, parentId: number): number[] {
|
||||
const childIds = [];
|
||||
for (const key in workItemInfos) {
|
||||
const workItem = workItemInfos[key];
|
||||
if (workItem.parent === parentId && workItem.level !== WorkItemLevel.Parent) {
|
||||
childIds.push(workItem.workItem.id);
|
||||
}
|
||||
}
|
||||
return childIds;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IWorkItemInfo, WorkItemLevel } from '../store/workitems/types';
|
||||
|
||||
// returns all work items for given level
|
||||
export function getWorkItemsForLevel(
|
||||
workItemInfos: IDictionaryNumberTo<IWorkItemInfo>,
|
||||
level: WorkItemLevel): number[] {
|
||||
|
||||
const allIds: string[] = Object.keys(workItemInfos);
|
||||
const ids = allIds.filter((id) => workItemInfos[Number(id)].level === level);
|
||||
|
||||
return ids.map(i => Number(i));
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { BacklogConfigurationReceivedAction, BacklogConfigurationReceivedType } from './actions';
|
||||
import { BacklogConfiguration } from 'TFS/Work/Contracts';
|
||||
|
||||
export const backlogConfigurationReceived: ActionCreator<BacklogConfigurationReceivedAction> =
|
||||
(projectId: string, teamId: string, backlogConfiguration: BacklogConfiguration) => ({
|
||||
type: BacklogConfigurationReceivedType,
|
||||
payload: {
|
||||
projectId,
|
||||
teamId,
|
||||
backlogConfiguration
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Action } from "redux";
|
||||
import { BacklogConfiguration } from "TFS/Work/Contracts";
|
||||
|
||||
export const BacklogConfigurationReceivedType = "@@backlogconfiguration/BacklogConfigurationReceived";
|
||||
|
||||
export interface BacklogConfigurationReceivedAction extends Action {
|
||||
type: "@@backlogconfiguration/BacklogConfigurationReceived";
|
||||
payload: {
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
backlogConfiguration: BacklogConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
export type BacklogConfigurationActions = BacklogConfigurationReceivedAction;
|
|
@ -0,0 +1,34 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { IBacklogConfigurationState } from './types';
|
||||
import { BacklogConfigurationActions, BacklogConfigurationReceivedType, BacklogConfigurationReceivedAction } from './actions';
|
||||
|
||||
// Type-safe initialState!
|
||||
export const initialState: IBacklogConfigurationState = {
|
||||
backlogConfigurations: {}
|
||||
};
|
||||
|
||||
const reducer: Reducer<IBacklogConfigurationState> = (state: IBacklogConfigurationState = initialState, action: BacklogConfigurationActions) => {
|
||||
switch (action.type) {
|
||||
case BacklogConfigurationReceivedType:
|
||||
return handleBacklogConfigurationReceived(state, action as BacklogConfigurationReceivedAction);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
function handleBacklogConfigurationReceived(state: IBacklogConfigurationState, action: BacklogConfigurationReceivedAction): IBacklogConfigurationState {
|
||||
let newState = { ...state };
|
||||
const {
|
||||
projectId,
|
||||
teamId,
|
||||
backlogConfiguration
|
||||
} = action.payload;
|
||||
|
||||
const projectData = newState.backlogConfigurations[projectId] ? { ...newState.backlogConfigurations[projectId] } : {};
|
||||
projectData[teamId] = backlogConfiguration;
|
||||
newState.backlogConfigurations[projectId] = projectData;
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,6 @@
|
|||
import { BacklogConfiguration } from "TFS/Work/Contracts";
|
||||
|
||||
export interface IBacklogConfigurationState {
|
||||
// project -> team -> Backlog Configuration
|
||||
backlogConfigurations: IDictionaryStringTo<IDictionaryStringTo<BacklogConfiguration>>;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { InitializeAction, InitializeType, ShowDetailsAction, ShowDetailsType, CloseDetailsAction, CloseDetailsType } from './actions';
|
||||
|
||||
export const createInitialize: ActionCreator<InitializeAction> =
|
||||
(projectId: string, teamId: string, backlogLevelName: string) => ({
|
||||
type: InitializeType,
|
||||
payload: {
|
||||
projectId,
|
||||
teamId,
|
||||
backlogLevelName
|
||||
}
|
||||
});
|
||||
|
||||
export const showDetails: ActionCreator<ShowDetailsAction> =
|
||||
(id: number) => ({
|
||||
type: ShowDetailsType,
|
||||
payload: { id }
|
||||
});
|
||||
|
||||
|
||||
export const closeDetails: ActionCreator<CloseDetailsAction> =
|
||||
(id: number) => ({
|
||||
type: CloseDetailsType,
|
||||
payload: { id }
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import { Action } from "redux";
|
||||
|
||||
export const InitializeType = "@@common/initialize";
|
||||
export const ShowDetailsType = "@@common/showdetails";
|
||||
export const CloseDetailsType = "@@common/closedetails";
|
||||
|
||||
export interface InitializeAction extends Action {
|
||||
type: "@@common/initialize";
|
||||
payload: {
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
backlogLevelName: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ShowDetailsAction extends Action {
|
||||
type: "@@common/showdetails";
|
||||
payload: {
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface CloseDetailsAction extends Action {
|
||||
type: "@@common/closedetails";
|
||||
payload: {
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type CommonActions = InitializeAction | ShowDetailsAction | CloseDetailsAction;
|
|
@ -0,0 +1,14 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { CommonActions, ShowDetailsType, CloseDetailsType } from './actions';
|
||||
const reducer: Reducer<number[]> = (state: number[] = [], action: CommonActions) => {
|
||||
switch (action.type) {
|
||||
case ShowDetailsType:
|
||||
return [...state, action.payload.id];
|
||||
case CloseDetailsType:
|
||||
return state.filter(id => id !== action.payload.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,11 @@
|
|||
export interface IContributionContext {
|
||||
level: string;
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
workItemTypes: string[];
|
||||
host: {
|
||||
background?: boolean;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { GenericErrorAction, GenericErrorType } from './actions';
|
||||
|
||||
export const genericError: ActionCreator<GenericErrorAction> =
|
||||
(error: string) => ({
|
||||
type: GenericErrorType,
|
||||
payload: error
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Action } from "redux";
|
||||
|
||||
export const GenericErrorType = "@@error/GenericError";
|
||||
|
||||
export interface GenericErrorAction extends Action {
|
||||
type: "@@error/GenericError";
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export type ErrorActions = GenericErrorAction;
|
|
@ -0,0 +1,15 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { ErrorActions, GenericErrorType } from './actions';
|
||||
const reducer: Reducer<string> = (state: string = "", action: ErrorActions) => {
|
||||
switch (action.type) {
|
||||
case GenericErrorType:
|
||||
if (typeof action.payload === "string") {
|
||||
return action.payload;
|
||||
}
|
||||
return action.payload["message"];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,73 @@
|
|||
import { combineReducers, Reducer } from 'redux';
|
||||
import { IWorkItemsState } from './workitems/types';
|
||||
import { IWorkItemMetadataState } from './workitemmetadata/types';
|
||||
import { ITeamSettingsIterationState } from './teamiterations/types';
|
||||
import { IBacklogConfigurationState } from './backlogconfiguration/types';
|
||||
|
||||
import workItemReducer from './workItems/reducer';
|
||||
import metadataReducer from './workitemmetadata/reducer';
|
||||
import teamIterationsReducer from './teamiterations/reducer';
|
||||
import errorReducer from './error/reducer';
|
||||
import loadingReducer from './loading/reducer';
|
||||
import backlogConfigurationReducer from './backlogconfiguration/reducer';
|
||||
import showHideDetailsReducer from "./common/reducer";
|
||||
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
|
||||
import savedOverriddenWorkItemIterationsReducer from "./workitems/overrideWorkItemIterationReducer";
|
||||
import overrideIterationReducer from "./overrideIterationProgress/reducer";
|
||||
|
||||
export interface IIterationDuration {
|
||||
startIteration: TeamSettingsIteration;
|
||||
endIteration: TeamSettingsIteration;
|
||||
kind: IterationDurationKind;
|
||||
overridedBy?: string; // User name for the case when kind is UserOverridden
|
||||
}
|
||||
|
||||
export enum IterationDurationKind {
|
||||
None,
|
||||
CurrentIteration,
|
||||
ChildRollup,
|
||||
UserOverridden
|
||||
}
|
||||
|
||||
export interface IOverriddenIterationDuration {
|
||||
startIterationId: string;
|
||||
endIterationId: string;
|
||||
user: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IWorkItemOverrideIteration {
|
||||
workItemId: number;
|
||||
iterationDuration: IOverriddenIterationDuration;
|
||||
changingStart: boolean; // Weather we are changing start iteration or end iteration
|
||||
}
|
||||
|
||||
export interface IFeatureTimelineRawState {
|
||||
workItemsState: IWorkItemsState;
|
||||
workItemMetadata: IWorkItemMetadataState;
|
||||
iterationState: ITeamSettingsIterationState;
|
||||
error: string;
|
||||
backlogConfiguration: IBacklogConfigurationState;
|
||||
loading: boolean;
|
||||
// This will contain any overridden iterations by the UI in extension storage
|
||||
savedOverriddenWorkItemIterations: IDictionaryNumberTo<IOverriddenIterationDuration>;
|
||||
|
||||
// list of work item ids for which the details window is shown
|
||||
workItemDetails: number[];
|
||||
|
||||
workItemOverrideIteration?: IWorkItemOverrideIteration;
|
||||
}
|
||||
|
||||
// setup reducers
|
||||
export const reducers: Reducer<IFeatureTimelineRawState> =
|
||||
combineReducers<IFeatureTimelineRawState>({
|
||||
workItemsState: workItemReducer,
|
||||
workItemMetadata: metadataReducer,
|
||||
iterationState: teamIterationsReducer,
|
||||
error: errorReducer,
|
||||
backlogConfiguration: backlogConfigurationReducer,
|
||||
loading: loadingReducer,
|
||||
workItemDetails: showHideDetailsReducer,
|
||||
workItemOverrideIteration: overrideIterationReducer,
|
||||
savedOverriddenWorkItemIterations: savedOverriddenWorkItemIterationsReducer
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { LoadingAction, LoadingType } from './actions';
|
||||
|
||||
export const loading: ActionCreator<LoadingAction> =
|
||||
(status: boolean) => ({
|
||||
type: LoadingType,
|
||||
payload: status
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Action } from "redux";
|
||||
|
||||
export const LoadingType = "@@loading/loading";
|
||||
|
||||
export interface LoadingAction extends Action {
|
||||
type: "@@loading/loading";
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
export type LoadingActions = LoadingAction;
|
|
@ -0,0 +1,12 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { LoadingActions, LoadingType } from './actions';
|
||||
const reducer: Reducer<boolean> = (state: boolean = false, action: LoadingActions) => {
|
||||
switch (action.type) {
|
||||
case LoadingType:
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,38 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { OverrideIterationStartAction, OverrideIterationStartType, OverrideIterationEndAction, OverrideIterationEndType, OverrideIterationHoverOverIterationAction, OverrideIterationHoverOverIterationType, OverrideIterationCleanupAction, OverrideIterationCleanupType, SaveOverrideIterationAction, SaveOverrideIterationActionType } from './actions';
|
||||
import { IWorkItemOverrideIteration } from '..';
|
||||
|
||||
export const startOverrideIteration: ActionCreator<OverrideIterationStartAction> =
|
||||
(payload: IWorkItemOverrideIteration) => ({
|
||||
type: OverrideIterationStartType,
|
||||
payload
|
||||
});
|
||||
|
||||
|
||||
export const endOverrideIteration: ActionCreator<OverrideIterationEndAction> =
|
||||
(payload: void) => ({
|
||||
type: OverrideIterationEndType,
|
||||
payload
|
||||
});
|
||||
|
||||
|
||||
export const cleanupOverrideIteration: ActionCreator<OverrideIterationCleanupAction> =
|
||||
(payload: void) => ({
|
||||
type: OverrideIterationCleanupType,
|
||||
payload
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const overrideHoverOverIteration: ActionCreator<OverrideIterationHoverOverIterationAction> =
|
||||
(payload: string) => ({
|
||||
type: OverrideIterationHoverOverIterationType,
|
||||
payload
|
||||
});
|
||||
|
||||
|
||||
export const saveOverrideIteration: ActionCreator<SaveOverrideIterationAction> =
|
||||
(payload: IWorkItemOverrideIteration) => ({
|
||||
type: SaveOverrideIterationActionType,
|
||||
payload
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { Action } from "redux";
|
||||
import { IWorkItemOverrideIteration } from "..";
|
||||
|
||||
export const OverrideIterationStartType = "@@overrideIteration/start";
|
||||
export const OverrideIterationEndType = "@@overrideIteration/end";
|
||||
export const SaveOverrideIterationActionType = "@@overrideIteration/save";
|
||||
export const OverrideIterationCleanupType = "@@overrideIteration/cleanup";
|
||||
export const OverrideIterationHoverOverIterationType = "@@overrideIteration/hoveroveriteration";
|
||||
|
||||
export interface OverrideIterationStartAction extends Action {
|
||||
type: "@@overrideIteration/start",
|
||||
payload: IWorkItemOverrideIteration
|
||||
}
|
||||
|
||||
export interface OverrideIterationEndAction extends Action {
|
||||
type: "@@overrideIteration/end",
|
||||
payload: void
|
||||
}
|
||||
|
||||
export interface SaveOverrideIterationAction extends Action {
|
||||
type: "@@overrideIteration/save",
|
||||
payload: IWorkItemOverrideIteration
|
||||
}
|
||||
|
||||
|
||||
export interface OverrideIterationCleanupAction extends Action {
|
||||
type: "@@overrideIteration/cleanup",
|
||||
payload: void
|
||||
}
|
||||
|
||||
export interface OverrideIterationHoverOverIterationAction extends Action {
|
||||
type: "@@overrideIteration/hoveroveriteration",
|
||||
payload: string
|
||||
}
|
||||
|
||||
export type OverrideIterationActions = OverrideIterationStartAction | OverrideIterationEndAction | OverrideIterationHoverOverIterationAction | OverrideIterationCleanupAction | SaveOverrideIterationAction;
|
|
@ -0,0 +1,29 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { OverrideIterationActions, OverrideIterationHoverOverIterationType, OverrideIterationStartType, OverrideIterationCleanupType } from './actions';
|
||||
import { IWorkItemOverrideIteration } from '..';
|
||||
const reducer: Reducer<IWorkItemOverrideIteration> = (state: IWorkItemOverrideIteration = null, action: OverrideIterationActions) => {
|
||||
switch (action.type) {
|
||||
case OverrideIterationStartType:
|
||||
return { ...action.payload };
|
||||
case OverrideIterationCleanupType:
|
||||
return null;
|
||||
case OverrideIterationHoverOverIterationType: {
|
||||
if (!state) {
|
||||
return state;
|
||||
}
|
||||
const newState = { ...state };
|
||||
newState.iterationDuration = { ...state.iterationDuration }
|
||||
if (newState.changingStart) {
|
||||
newState.iterationDuration.startIterationId = action.payload;
|
||||
} else {
|
||||
newState.iterationDuration.endIterationId = action.payload;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,52 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { TeamSettingsIterationReceivedAction, TeamSettingsIterationReceivedType, DisplayAllIterationsAction, DisplayAllIterationsActionType, ChangeDisplayIterationCountAction, ChangeDisplayIterationCountActionType, ShiftDisplayIterationLeftActionType, ShiftDisplayIterationLeftAction, ShiftDisplayIterationRightActionType, ShiftDisplayIterationRightAction, RestoreDisplayIterationCountAction, RestoreDisplayIterationCountActionType } from './actions';
|
||||
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
|
||||
import { IIterationDisplayOptions } from './types';
|
||||
|
||||
export const teamSettingsIterationReceived: ActionCreator<TeamSettingsIterationReceivedAction> =
|
||||
(projectId: string, teamId: string, TeamSettingsIterations: TeamSettingsIteration[]) => ({
|
||||
type: TeamSettingsIterationReceivedType,
|
||||
payload: {
|
||||
projectId,
|
||||
teamId,
|
||||
TeamSettingsIterations
|
||||
}
|
||||
});
|
||||
|
||||
export const displayAllIterations: ActionCreator<DisplayAllIterationsAction> =
|
||||
() => ({
|
||||
type: DisplayAllIterationsActionType,
|
||||
payload: null
|
||||
});
|
||||
|
||||
export const changeDisplayIterationCount: ActionCreator<ChangeDisplayIterationCountAction> =
|
||||
(count: number, projectId: string, teamId: string) => ({
|
||||
type: ChangeDisplayIterationCountActionType,
|
||||
payload: {
|
||||
count,
|
||||
projectId,
|
||||
teamId
|
||||
}
|
||||
});
|
||||
|
||||
export const restoreDisplayIterationCount: ActionCreator<RestoreDisplayIterationCountAction> =
|
||||
(payload: IIterationDisplayOptions) => ({
|
||||
type: RestoreDisplayIterationCountActionType,
|
||||
payload
|
||||
});
|
||||
|
||||
export const shiftDisplayIterationLeft: ActionCreator<ShiftDisplayIterationLeftAction> =
|
||||
(count: number) => ({
|
||||
type: ShiftDisplayIterationLeftActionType,
|
||||
payload: {
|
||||
count,
|
||||
}
|
||||
});
|
||||
|
||||
export const shiftDisplayIterationRight: ActionCreator<ShiftDisplayIterationRightAction> =
|
||||
(count: number) => ({
|
||||
type: ShiftDisplayIterationRightActionType,
|
||||
payload: {
|
||||
count,
|
||||
}
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Action } from "redux";
|
||||
import { TeamSettingsIteration } from "TFS/Work/Contracts";
|
||||
import { IIterationDisplayOptions } from "./types";
|
||||
|
||||
export const TeamSettingsIterationReceivedType = "@@TeamSettingsIteration/TeamSettingsIterationReceived";
|
||||
export const ChangeDisplayIterationCountActionType = "@@TeamSettingsIteration/ChangeDisplayIterationCountAction";
|
||||
export const RestoreDisplayIterationCountActionType = "@@TeamSettingsIteration/RestoreDisplayIterationCountAction";
|
||||
export const ShiftDisplayIterationLeftActionType = "@@TeamSettingsIteration/ShiftDisplayIterationLeftAction";
|
||||
export const ShiftDisplayIterationRightActionType = "@@TeamSettingsIteration/ShiftDisplayIterationRightAction";
|
||||
export const DisplayAllIterationsActionType = "@@TeamSettingsIteration/DisplayAllIterationsAction";
|
||||
|
||||
export interface TeamSettingsIterationReceivedAction extends Action {
|
||||
type: "@@TeamSettingsIteration/TeamSettingsIterationReceived";
|
||||
payload: {
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
TeamSettingsIterations: TeamSettingsIteration[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface DisplayAllIterationsAction extends Action {
|
||||
type: "@@TeamSettingsIteration/DisplayAllIterationsAction";
|
||||
payload: void;
|
||||
}
|
||||
|
||||
export interface ChangeDisplayIterationCountAction extends Action {
|
||||
type: "@@TeamSettingsIteration/ChangeDisplayIterationCountAction";
|
||||
payload: {
|
||||
count: number,
|
||||
teamId: string,
|
||||
projectId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RestoreDisplayIterationCountAction extends Action {
|
||||
type: "@@TeamSettingsIteration/RestoreDisplayIterationCountAction";
|
||||
payload: IIterationDisplayOptions
|
||||
}
|
||||
|
||||
export interface ShiftDisplayIterationLeftAction extends Action {
|
||||
type: "@@TeamSettingsIteration/ShiftDisplayIterationLeftAction";
|
||||
payload: {
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ShiftDisplayIterationRightAction extends Action {
|
||||
type: "@@TeamSettingsIteration/ShiftDisplayIterationRightAction";
|
||||
payload: {
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export type TeamSettingsIterationActions = TeamSettingsIterationReceivedAction | DisplayAllIterationsAction | ChangeDisplayIterationCountAction | ShiftDisplayIterationLeftAction | ShiftDisplayIterationRightAction | RestoreDisplayIterationCountAction;
|
|
@ -0,0 +1,158 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { ITeamSettingsIterationState } from './types';
|
||||
import { TeamSettingsIterationActions, TeamSettingsIterationReceivedType, TeamSettingsIterationReceivedAction, DisplayAllIterationsActionType, ShiftDisplayIterationLeftActionType, ShiftDisplayIterationRightActionType, ChangeDisplayIterationCountActionType, ShiftDisplayIterationLeftAction, ShiftDisplayIterationRightAction, ChangeDisplayIterationCountAction, RestoreDisplayIterationCountActionType, RestoreDisplayIterationCountAction } from './actions';
|
||||
import { TeamSettingsIteration, TimeFrame } from 'TFS/Work/Contracts';
|
||||
|
||||
// Type-safe initialState!
|
||||
export const initialState: ITeamSettingsIterationState = {
|
||||
// project -> team -> teamsettingsiterations
|
||||
teamSettingsIterations: {},
|
||||
iterationDisplayOptions: null
|
||||
};
|
||||
|
||||
const reducer: Reducer<ITeamSettingsIterationState> = (state: ITeamSettingsIterationState = initialState,
|
||||
action: TeamSettingsIterationActions) => {
|
||||
switch (action.type) {
|
||||
case TeamSettingsIterationReceivedType:
|
||||
return handleTeamSettingsIterationReceived(state, action as TeamSettingsIterationReceivedAction);
|
||||
case DisplayAllIterationsActionType:
|
||||
return handleDisplayAllIterations(state);
|
||||
case ShiftDisplayIterationLeftActionType:
|
||||
return handleShiftDisplayIterationLeft(state, action);
|
||||
case ShiftDisplayIterationRightActionType:
|
||||
return handleShiftDisplayIterationRight(state, action);
|
||||
case ChangeDisplayIterationCountActionType:
|
||||
return handleChangeDisplayIterationCountAction(state, action);
|
||||
case RestoreDisplayIterationCountActionType:
|
||||
return handleRestoreDisplayIterationCountAction(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
function handleRestoreDisplayIterationCountAction(state: ITeamSettingsIterationState, action: RestoreDisplayIterationCountAction) {
|
||||
const newState = { ...state };
|
||||
newState.iterationDisplayOptions = action.payload;
|
||||
let { count } = action.payload;
|
||||
|
||||
const iterations = state.teamSettingsIterations[action.payload.projectId][action.payload.teamId] || [];
|
||||
|
||||
// Handle incase if the team iterations changed before restore
|
||||
if (count > iterations.length) {
|
||||
const currentIterationIndex = (iterations && getCurrentIterationIndex(iterations)) || 0;
|
||||
|
||||
count = iterations.length;
|
||||
|
||||
let startIndex = currentIterationIndex - Math.floor((count / 2));
|
||||
if (startIndex < 0) {
|
||||
startIndex = 0;
|
||||
}
|
||||
const endIndex = startIndex + (count - 1);
|
||||
|
||||
newState.iterationDisplayOptions.count = count;
|
||||
newState.iterationDisplayOptions.startIndex = startIndex;
|
||||
newState.iterationDisplayOptions.endIndex = endIndex;
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function handleChangeDisplayIterationCountAction(state: ITeamSettingsIterationState, action: ChangeDisplayIterationCountAction) {
|
||||
let {
|
||||
count,
|
||||
teamId,
|
||||
projectId
|
||||
} = action.payload;
|
||||
|
||||
const originalCount = count;
|
||||
|
||||
const iterations = state.teamSettingsIterations[projectId][teamId];
|
||||
const currentIterationIndex = getCurrentIterationIndex(iterations);
|
||||
|
||||
if (count > iterations.length) {
|
||||
count = iterations.length;
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
count,
|
||||
originalCount,
|
||||
teamId,
|
||||
projectId,
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
totalIterations: iterations.length
|
||||
};
|
||||
|
||||
let startIndex = currentIterationIndex - Math.floor((count / 2));
|
||||
if (startIndex < 0) {
|
||||
startIndex = 0;
|
||||
}
|
||||
const endIndex = startIndex + (count - 1);
|
||||
|
||||
displayOptions.startIndex = startIndex;
|
||||
displayOptions.endIndex = endIndex;
|
||||
|
||||
const newState = { ...state };
|
||||
newState.iterationDisplayOptions = displayOptions;
|
||||
return newState;
|
||||
}
|
||||
|
||||
function getCurrentIterationIndex(iterations: TeamSettingsIteration[]): number {
|
||||
if (TimeFrame) {
|
||||
return iterations.findIndex(i => i.attributes.timeFrame === TimeFrame.Current);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function handleShiftDisplayIterationLeft(state: ITeamSettingsIterationState, action: ShiftDisplayIterationLeftAction) {
|
||||
if (state.iterationDisplayOptions) {
|
||||
const newState = { ...state };
|
||||
|
||||
const displayOptions = { ...newState.iterationDisplayOptions };
|
||||
if ((displayOptions.startIndex - action.payload.count) >= 0) {
|
||||
displayOptions.startIndex -= action.payload.count;
|
||||
displayOptions.endIndex = displayOptions.startIndex + state.iterationDisplayOptions.count - 1;
|
||||
}
|
||||
newState.iterationDisplayOptions = displayOptions
|
||||
return newState;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function handleShiftDisplayIterationRight(state: ITeamSettingsIterationState, action: ShiftDisplayIterationRightAction) {
|
||||
if (state.iterationDisplayOptions) {
|
||||
const newState = { ...state };
|
||||
const iterationCount = state.teamSettingsIterations[state.iterationDisplayOptions.projectId][state.iterationDisplayOptions.teamId].length;
|
||||
const displayOptions = { ...newState.iterationDisplayOptions };
|
||||
if ((displayOptions.endIndex + action.payload.count) < iterationCount) {
|
||||
displayOptions.endIndex += action.payload.count;
|
||||
displayOptions.startIndex = displayOptions.endIndex - state.iterationDisplayOptions.count + 1;
|
||||
}
|
||||
newState.iterationDisplayOptions = displayOptions;
|
||||
return newState;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function handleDisplayAllIterations(state: ITeamSettingsIterationState) {
|
||||
const newState = { ...state };
|
||||
newState.iterationDisplayOptions = null;
|
||||
return newState;
|
||||
}
|
||||
|
||||
function handleTeamSettingsIterationReceived(state: ITeamSettingsIterationState, action: TeamSettingsIterationReceivedAction): ITeamSettingsIterationState {
|
||||
let newState = { ...state };
|
||||
const {
|
||||
projectId,
|
||||
teamId,
|
||||
TeamSettingsIterations
|
||||
} = action.payload;
|
||||
|
||||
const projectData = newState.teamSettingsIterations[projectId] ? { ...newState.teamSettingsIterations[projectId] } : {};
|
||||
projectData[teamId] = TeamSettingsIterations;
|
||||
newState.teamSettingsIterations[projectId] = projectData;
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,17 @@
|
|||
import { TeamSettingsIteration } from "TFS/Work/Contracts";
|
||||
|
||||
export interface IIterationDisplayOptions {
|
||||
totalIterations: number
|
||||
originalCount: number;
|
||||
count: number;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export interface ITeamSettingsIterationState {
|
||||
// project -> team -> Backlog Configuration
|
||||
teamSettingsIterations: IDictionaryStringTo<IDictionaryStringTo<TeamSettingsIteration[]>>;
|
||||
iterationDisplayOptions: IIterationDisplayOptions;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import { WorkItemTypesReceivedAction, WorkItemTypesReceivedActionType, WorkItemStateColorsReceivedAction, WorkItemStateColorsReceivedActionType } from './actions';
|
||||
import { WorkItemType, WorkItemStateColor } from 'TFS/WorkItemTracking/Contracts';
|
||||
|
||||
export const workItemTypesReceived: ActionCreator<WorkItemTypesReceivedAction> =
|
||||
(projectId: string, workItemTypes: WorkItemType[]) => ({
|
||||
type: WorkItemTypesReceivedActionType,
|
||||
payload: {
|
||||
projectId,
|
||||
workItemTypes
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const workItemStateColorsReceived: ActionCreator<WorkItemStateColorsReceivedAction> =
|
||||
(projectId: string, workItemTypeStateColors: IDictionaryStringTo<WorkItemStateColor[]>) => ({
|
||||
type: WorkItemStateColorsReceivedActionType,
|
||||
payload: {
|
||||
projectId,
|
||||
workItemTypeStateColors
|
||||
}
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import { Action } from 'redux';
|
||||
import { WorkItemType, WorkItemStateColor } from 'TFS/WorkItemTracking/Contracts';
|
||||
|
||||
export const WorkItemTypesReceivedActionType = '@@workitemmetadata/WorkItemTypesReceived';
|
||||
export const WorkItemStateColorsReceivedActionType = '@@workitemmetadata/WorkItemStateColorsReceived';
|
||||
|
||||
export interface WorkItemTypesReceivedAction extends Action {
|
||||
type: '@@workitemmetadata/WorkItemTypesReceived';
|
||||
payload: {
|
||||
projectId: string;
|
||||
workItemTypes: WorkItemType[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkItemStateColorsReceivedAction extends Action {
|
||||
type: '@@workitemmetadata/WorkItemStateColorsReceived';
|
||||
payload: {
|
||||
projectId: string;
|
||||
workItemTypeStateColors: IDictionaryStringTo<WorkItemStateColor[]>;
|
||||
}
|
||||
}
|
||||
|
||||
export type MetaDataActions = WorkItemTypesReceivedAction | WorkItemStateColorsReceivedAction;
|
|
@ -0,0 +1,48 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { IWorkItemMetadataState, IWorkItemMetadata } from './types';
|
||||
import { WorkItemTypesReceivedActionType, MetaDataActions, WorkItemTypesReceivedAction, WorkItemStateColorsReceivedAction, WorkItemStateColorsReceivedActionType } from './actions';
|
||||
|
||||
// Type-safe initialState!
|
||||
export const initialState: IWorkItemMetadataState = {
|
||||
// project -> metadata
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const reducer: Reducer<IWorkItemMetadataState> = (state: IWorkItemMetadataState = initialState, action: MetaDataActions) => {
|
||||
switch (action.type) {
|
||||
case WorkItemTypesReceivedActionType:
|
||||
return handleWorkItemTypesReceived(state, action as WorkItemTypesReceivedAction);
|
||||
case WorkItemStateColorsReceivedActionType:
|
||||
return handleWorkItemStateColorsReceived(state, action as WorkItemStateColorsReceivedAction);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
function handleWorkItemTypesReceived(state: IWorkItemMetadataState, action: WorkItemTypesReceivedAction): IWorkItemMetadataState {
|
||||
let newState = { ...state };
|
||||
const {
|
||||
projectId,
|
||||
workItemTypes
|
||||
} = action.payload;
|
||||
|
||||
const projectData: IWorkItemMetadata = newState.metadata[projectId] ? { ...newState.metadata[projectId] } : {} as IWorkItemMetadata;
|
||||
projectData.workItemTypes = workItemTypes;
|
||||
newState.metadata[projectId] = projectData;
|
||||
return newState;
|
||||
}
|
||||
|
||||
function handleWorkItemStateColorsReceived(state: IWorkItemMetadataState, action: WorkItemStateColorsReceivedAction): IWorkItemMetadataState {
|
||||
let newState = { ...state };
|
||||
const {
|
||||
projectId,
|
||||
workItemTypeStateColors
|
||||
} = action.payload;
|
||||
|
||||
const projectData: IWorkItemMetadata = newState.metadata[projectId] ? { ...newState.metadata[projectId] } : {} as IWorkItemMetadata;
|
||||
projectData.workItemStateColors = workItemTypeStateColors;
|
||||
newState.metadata[projectId] = projectData;
|
||||
return newState;
|
||||
}
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,11 @@
|
|||
import { WorkItemType, WorkItemStateColor } from "TFS/WorkItemTracking/Contracts";
|
||||
|
||||
export interface IWorkItemMetadata {
|
||||
workItemTypes: WorkItemType[];
|
||||
workItemStateColors: IDictionaryStringTo<WorkItemStateColor[]>;
|
||||
}
|
||||
|
||||
export interface IWorkItemMetadataState {
|
||||
// project -> WorkItemMetadata
|
||||
metadata: IDictionaryStringTo<IWorkItemMetadata>;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`test work item received 1`] = `
|
||||
Object {
|
||||
"workItemInfos": Object {
|
||||
"1": Object {
|
||||
"children": Array [],
|
||||
"level": 1,
|
||||
"parent": 0,
|
||||
"workItem": Object {
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`work item updated 1`] = `
|
||||
Object {
|
||||
"workItemInfos": Object {
|
||||
"1": Object {
|
||||
"children": Array [],
|
||||
"level": 2,
|
||||
"parent": 0,
|
||||
"workItem": Object {
|
||||
"fields": Object {
|
||||
"System.Title": "hello",
|
||||
},
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`work item updated 2`] = `
|
||||
Object {
|
||||
"workItemInfos": Object {
|
||||
"1": Object {
|
||||
"children": Array [],
|
||||
"level": 2,
|
||||
"parent": 0,
|
||||
"workItem": Object {
|
||||
"fields": Object {
|
||||
"System.Title": "hi",
|
||||
},
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,36 @@
|
|||
declare var it, expect;
|
||||
|
||||
import reducer from '../reducer';
|
||||
import { workItemsReceived, replaceWorkItems } from '../actionCreators';
|
||||
debugger;
|
||||
it('test work item received', () => {
|
||||
debugger;
|
||||
const action = workItemsReceived([{
|
||||
id: 1
|
||||
}], [], [1], [])
|
||||
const state = reducer({workItemInfos: {}}, action);
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('work item updated', () => {
|
||||
const action = workItemsReceived([{
|
||||
id: 1,
|
||||
fields: {
|
||||
"System.Title": "hello"
|
||||
}
|
||||
}], [], [2], []);
|
||||
|
||||
let state = reducer({workItemInfos: {}}, action);
|
||||
expect(state).toMatchSnapshot();
|
||||
|
||||
const replaceAction = replaceWorkItems([{
|
||||
id: 1,
|
||||
fields: {
|
||||
"System.Title": "hi"
|
||||
}
|
||||
|
||||
}])
|
||||
state = reducer(state, replaceAction);
|
||||
expect(state).toMatchSnapshot();
|
||||
});
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { ActionCreator } from 'redux';
|
||||
import {
|
||||
StartUpdateWorkitemIterationAction, WorkItemLinksReceivedActionType,
|
||||
StartUpdateWorkitemIterationActionType, ChangeParentAction,
|
||||
ChangeParentActionType, WorkItemsReceivedAction,
|
||||
WorkItemsReceivedActionType, WorkItemLinksReceivedAction,
|
||||
ReplaceWorkItemsAction, ReplaceWorkItemsActionType, LaunchWorkItemFormAction,
|
||||
LaunchWorkItemFormActionType, SetOverrideIterationAction, SetOverrideIterationType, ClearOverrideIterationAction, ClearOverrideIterationType, WorkItemSavedAction, WorkItemSavedActionType, WorkItemSaveFailedActionType, WorkItemSaveFailedAction
|
||||
} from './actions';
|
||||
|
||||
import { WorkItem, WorkItemLink } from 'TFS/WorkItemTracking/Contracts';
|
||||
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
|
||||
|
||||
export const startUpdateWorkItemIteration: ActionCreator<StartUpdateWorkitemIterationAction> =
|
||||
(workItem: number, teamIteration: TeamSettingsIteration, override: boolean) => ({
|
||||
type: StartUpdateWorkitemIterationActionType,
|
||||
payload: {
|
||||
workItem,
|
||||
teamIteration,
|
||||
override
|
||||
}
|
||||
});
|
||||
|
||||
export const workItemSaved: ActionCreator<WorkItemSavedAction> =
|
||||
(workItems: number[]) => ({
|
||||
type: WorkItemSavedActionType,
|
||||
payload: {
|
||||
workItems
|
||||
}
|
||||
});
|
||||
|
||||
export const workItemSaveFailed: ActionCreator<WorkItemSaveFailedAction> =
|
||||
(workItems: number[], error: string) => ({
|
||||
type: WorkItemSaveFailedActionType,
|
||||
payload: {
|
||||
workItems,
|
||||
error
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const changParent: ActionCreator<ChangeParentAction> =
|
||||
(workItems: number[], oldParent?: number, newParentId?: number) => ({
|
||||
type: ChangeParentActionType,
|
||||
payload: {
|
||||
workItems,
|
||||
oldParent,
|
||||
newParentId
|
||||
}
|
||||
});
|
||||
|
||||
export const workItemsReceived: ActionCreator<WorkItemsReceivedAction> =
|
||||
(workItems: WorkItem[],
|
||||
parentWorkItemIds: number[],
|
||||
currentLevelWorkItemIds: number[],
|
||||
childLevelWorkItemIds: number[]) => ({
|
||||
type: WorkItemsReceivedActionType,
|
||||
payload: {
|
||||
workItems,
|
||||
parentWorkItemIds,
|
||||
currentLevelWorkItemIds,
|
||||
childLevelWorkItemIds,
|
||||
}
|
||||
});
|
||||
|
||||
export const workItemLinksReceived: ActionCreator<WorkItemLinksReceivedAction> =
|
||||
(workItemLinks: WorkItemLink[]) => ({
|
||||
type: WorkItemLinksReceivedActionType,
|
||||
payload: {
|
||||
workItemLinks
|
||||
}
|
||||
});
|
||||
|
||||
export const replaceWorkItems: ActionCreator<ReplaceWorkItemsAction> =
|
||||
(workItems: WorkItem[]) => ({
|
||||
type: ReplaceWorkItemsActionType,
|
||||
payload: {
|
||||
workItems
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const launchWorkItemForm: ActionCreator<LaunchWorkItemFormAction> =
|
||||
(workItemId: number) => ({
|
||||
type: LaunchWorkItemFormActionType,
|
||||
track: true,
|
||||
payload: {
|
||||
workItemId
|
||||
}
|
||||
});
|
||||
|
||||
export const setOverrideIteration: ActionCreator<SetOverrideIterationAction> =
|
||||
(workItemId: number, startIterationId: string, endIterationId: string, user: string) => ({
|
||||
type: SetOverrideIterationType,
|
||||
track: true,
|
||||
payload: {
|
||||
workItemId,
|
||||
startIterationId,
|
||||
endIterationId,
|
||||
user
|
||||
}
|
||||
});
|
||||
export const clearOverrideIteration: ActionCreator<ClearOverrideIterationAction> =
|
||||
(id: number) => ({
|
||||
type: ClearOverrideIterationType,
|
||||
payload: id
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
import { Action } from "redux";
|
||||
import { WorkItem, WorkItemLink } from "TFS/WorkItemTracking/Contracts";
|
||||
import { TrackableAction } from "./types";
|
||||
import { TeamSettingsIteration } from "TFS/Work/Contracts";
|
||||
export const StartUpdateWorkitemIterationActionType = "@@workitems/StartUpdateWorkitemIterationAction";
|
||||
export const WorkItemSavedActionType = "@@workitems/WorkItemSavedAction";
|
||||
export const WorkItemSaveFailedActionType = "@@workitems/WorkItemSaveFailedAction";
|
||||
export const ChangeParentActionType = "@@workitems/ChangeParentAction";
|
||||
export const WorkItemsReceivedActionType = "@@workitems/WorkItemsReceived";
|
||||
export const WorkItemLinksReceivedActionType = "@@workitems/WorkItemLinksReceived"
|
||||
export const ReplaceWorkItemsActionType = "@@workitems/ReplaceWorkItems";
|
||||
export const LaunchWorkItemFormActionType = "@@workitems/LaunchWorkItemForm";
|
||||
export const SetOverrideIterationType = "@@workitems/setoverrideiteration";
|
||||
export const ClearOverrideIterationType = "@@overrideIteration/cleareoverrideiteration";
|
||||
|
||||
export interface StartUpdateWorkitemIterationAction extends Action {
|
||||
type: "@@workitems/StartUpdateWorkitemIterationAction";
|
||||
payload: {
|
||||
workItem: number;
|
||||
teamIteration: TeamSettingsIteration;
|
||||
override: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkItemSavedAction extends Action {
|
||||
type: "@@workitems/WorkItemSavedAction";
|
||||
payload: {
|
||||
workItems: number[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkItemSaveFailedAction extends Action {
|
||||
type: "@@workitems/WorkItemSaveFailedAction";
|
||||
payload: {
|
||||
workItems: number[];
|
||||
error: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChangeParentAction extends Action {
|
||||
type: "@@workitems/ChangeParentAction";
|
||||
payload: {
|
||||
workItems: number[];
|
||||
newParentId?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkItemsReceivedAction extends Action {
|
||||
type: "@@workitems/WorkItemsReceived";
|
||||
payload: {
|
||||
workItems: WorkItem[];
|
||||
parentWorkItemIds: number[];
|
||||
currentLevelWorkItemIds: number[];
|
||||
childLevelWorkItemIds: number[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkItemLinksReceivedAction extends Action {
|
||||
type: "@@workitems/WorkItemLinksReceived";
|
||||
payload: {
|
||||
workItemLinks: WorkItemLink[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReplaceWorkItemsAction extends Action {
|
||||
type: "@@workitems/ReplaceWorkItems";
|
||||
payload: {
|
||||
workItems: WorkItem[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface LaunchWorkItemFormAction extends TrackableAction {
|
||||
type: "@@workitems/LaunchWorkItemForm";
|
||||
payload: {
|
||||
workItemId: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface SetOverrideIterationAction extends TrackableAction {
|
||||
type: "@@workitems/setoverrideiteration";
|
||||
payload: {
|
||||
workItemId: number;
|
||||
startIterationId: string;
|
||||
endIterationId: string;
|
||||
user: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClearOverrideIterationAction extends Action {
|
||||
type: "@@overrideIteration/cleareoverrideiteration",
|
||||
payload: number
|
||||
}
|
||||
|
||||
|
||||
export type WorkItemActions = StartUpdateWorkitemIterationAction | ChangeParentAction | WorkItemsReceivedAction | WorkItemLinksReceivedAction | ReplaceWorkItemsAction | LaunchWorkItemFormAction;
|
||||
export type OverrideIterationActions = SetOverrideIterationAction | ClearOverrideIterationAction;
|
|
@ -0,0 +1,29 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { IOverriddenIterationDuration } from '..';
|
||||
import { SetOverrideIterationType, ClearOverrideIterationType, OverrideIterationActions } from './actions';
|
||||
|
||||
const reducer: Reducer<IDictionaryNumberTo<IOverriddenIterationDuration>> =
|
||||
(state: IDictionaryNumberTo<IOverriddenIterationDuration> = {}, action: OverrideIterationActions) => {
|
||||
switch (action.type) {
|
||||
case SetOverrideIterationType:
|
||||
const newState = { ...state };
|
||||
newState[Number(action.payload.workItemId)] = {
|
||||
startIterationId: action.payload.startIterationId,
|
||||
endIterationId: action.payload.endIterationId,
|
||||
user: action.payload.user
|
||||
};
|
||||
return newState;
|
||||
case ClearOverrideIterationType: {
|
||||
if (!state) {
|
||||
return state;
|
||||
}
|
||||
const newState = { ...state };
|
||||
delete newState[action.payload]
|
||||
return newState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,165 @@
|
|||
import {
|
||||
ChangeParentAction,
|
||||
ChangeParentActionType,
|
||||
ReplaceWorkItemsAction,
|
||||
ReplaceWorkItemsActionType,
|
||||
WorkItemActions,
|
||||
WorkItemsReceivedAction,
|
||||
WorkItemsReceivedActionType,
|
||||
WorkItemLinksReceivedAction,
|
||||
WorkItemLinksReceivedActionType,
|
||||
StartUpdateWorkitemIterationAction,
|
||||
StartUpdateWorkitemIterationActionType
|
||||
} from './actions';
|
||||
import { IWorkItemsState, WorkItemLevel } from './types';
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
// Type-safe initialState!
|
||||
export const initialState: IWorkItemsState = {
|
||||
workItemInfos: {}
|
||||
};
|
||||
|
||||
const reducer: Reducer<IWorkItemsState> = (state: IWorkItemsState = initialState, action: WorkItemActions) => {
|
||||
switch (action.type) {
|
||||
case ChangeParentActionType:
|
||||
return handleChangeParent(state, action as ChangeParentAction);
|
||||
case WorkItemsReceivedActionType:
|
||||
return handleWorkItemsReceived(state, action as WorkItemsReceivedAction);
|
||||
case ReplaceWorkItemsActionType:
|
||||
return handleReplaceWorkItems(state, action as ReplaceWorkItemsAction);
|
||||
case WorkItemLinksReceivedActionType:
|
||||
return handleWorkItemLinksReceived(state, action as WorkItemLinksReceivedAction);
|
||||
case StartUpdateWorkitemIterationActionType:
|
||||
return handleStartUpdateWorkItemIteration(state, action as StartUpdateWorkitemIterationAction);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
function handleStartUpdateWorkItemIteration(state: IWorkItemsState, action: StartUpdateWorkitemIterationAction): IWorkItemsState {
|
||||
const {
|
||||
workItem,
|
||||
teamIteration
|
||||
} = action.payload;
|
||||
|
||||
const newState = { ...state };
|
||||
const workItemObject = newState.workItemInfos[workItem];
|
||||
if (workItemObject) {
|
||||
workItemObject.workItem.fields = { ...workItemObject.workItem.fields };
|
||||
workItemObject.workItem.fields["System.IterationPath"] = teamIteration.path;
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function handleChangeParent(state: IWorkItemsState, action: ChangeParentAction): IWorkItemsState {
|
||||
const {
|
||||
workItems,
|
||||
newParentId
|
||||
} = action.payload;
|
||||
|
||||
const newState = { ...state };
|
||||
for (const childId of workItems) {
|
||||
changeParent(newState, childId, newParentId);
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function changeParent(newState: IWorkItemsState, childId: number, parentId: number) {
|
||||
const info = newState.workItemInfos[childId];
|
||||
const oldParentId = info.parent;
|
||||
if (parentId === oldParentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newParentInfo = newState.workItemInfos[parentId];
|
||||
|
||||
// Remove work item from old parent
|
||||
if (oldParentId) {
|
||||
const oldParentInfo = newState[oldParentId];
|
||||
newState.workItemInfos[oldParentId] = {
|
||||
...oldParentInfo
|
||||
};
|
||||
newState.workItemInfos[oldParentId].children = oldParentInfo.children.filter((id) => id !== childId);
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
//Add workItem as child of new parent
|
||||
newState.workItemInfos[parentId] = {
|
||||
...newParentInfo
|
||||
};
|
||||
|
||||
newState.workItemInfos[parentId].children = [...newParentInfo.children, childId];
|
||||
};
|
||||
|
||||
|
||||
//Set parent id
|
||||
newState.workItemInfos[childId] = {
|
||||
...newState.workItemInfos[childId],
|
||||
parent: parentId
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function handleWorkItemsReceived(state: IWorkItemsState, action: WorkItemsReceivedAction): IWorkItemsState {
|
||||
const newState = { ...state };
|
||||
const {
|
||||
workItems,
|
||||
parentWorkItemIds,
|
||||
currentLevelWorkItemIds
|
||||
} = action.payload;
|
||||
|
||||
for (const workItem of workItems) {
|
||||
|
||||
let level = WorkItemLevel.Child;
|
||||
if (parentWorkItemIds.some(parentId => parentId === workItem.id)) {
|
||||
level = WorkItemLevel.Parent;
|
||||
} else if (currentLevelWorkItemIds.some(currentId => currentId === workItem.id)) {
|
||||
level = WorkItemLevel.Current;
|
||||
}
|
||||
|
||||
newState.workItemInfos[workItem.id] = {
|
||||
workItem,
|
||||
children: [],
|
||||
parent: 0,
|
||||
level
|
||||
};
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
function handleReplaceWorkItems(state: IWorkItemsState, action: ReplaceWorkItemsAction): IWorkItemsState {
|
||||
const newState = { ...state };
|
||||
const {
|
||||
workItems
|
||||
} = action.payload;
|
||||
|
||||
for (const workItem of workItems) {
|
||||
newState.workItemInfos[workItem.id] = {
|
||||
...newState.workItemInfos[workItem.id],
|
||||
workItem: workItem
|
||||
};
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
function handleWorkItemLinksReceived(state: IWorkItemsState, action: WorkItemLinksReceivedAction): IWorkItemsState {
|
||||
const newState = { ...state };
|
||||
const children = action.payload.workItemLinks.filter((link) => link.source);
|
||||
for (const relation of children) {
|
||||
let parentId = 0; relation.source.id;
|
||||
let childId = 0; relation.target.id;
|
||||
if (relation.rel === "System.LinkTypes.Hierarchy-Forward") {
|
||||
parentId = relation.source.id;
|
||||
childId = relation.target.id;
|
||||
} else if (relation.rel === "System.LinkTypes.Hierarchy-Reverse") {
|
||||
parentId = relation.target.id;
|
||||
childId = relation.source.id;
|
||||
}
|
||||
changeParent(newState, childId, parentId);
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,23 @@
|
|||
import { WorkItem } from "TFS/WorkItemTracking/Contracts";
|
||||
import { Action } from "redux";
|
||||
|
||||
export enum WorkItemLevel {
|
||||
Parent,
|
||||
Current,
|
||||
Child
|
||||
}
|
||||
|
||||
export interface IWorkItemInfo {
|
||||
workItem: WorkItem;
|
||||
children: number[];
|
||||
parent: number;
|
||||
level: WorkItemLevel;
|
||||
}
|
||||
|
||||
export interface IWorkItemsState {
|
||||
workItemInfos: IDictionaryNumberTo<IWorkItemInfo>;
|
||||
}
|
||||
|
||||
export interface TrackableAction extends Action {
|
||||
track: boolean;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
export enum UIStatus {
|
||||
Default,
|
||||
Loading,
|
||||
Error,
|
||||
NoWorkItems,
|
||||
NoTeamIterations,
|
||||
}
|
||||
|
||||
export enum CropWorkItem {
|
||||
None,
|
||||
Left,
|
||||
Right,
|
||||
Both
|
||||
}
|
||||
|
||||
export interface IDimension {
|
||||
startRow: number;
|
||||
startCol: number;
|
||||
|
||||
endRow: number;
|
||||
endCol: number;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "amd",
|
||||
"sourceMap": false,
|
||||
"noUnusedLocals": true,
|
||||
"jsx": "react",
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"es2015",
|
||||
"es2015.promise",
|
||||
"dom"
|
||||
],
|
||||
"target":"es5",
|
||||
"moduleResolution": "node",
|
||||
"types": [
|
||||
"requirejs",
|
||||
"react",
|
||||
"react-dom",
|
||||
"vss-web-extension-sdk"
|
||||
],
|
||||
"typeRoots": [
|
||||
"../node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
{
|
||||
"extends": [
|
||||
"tslint:latest",
|
||||
"tslint:recommended",
|
||||
"tslint-react",
|
||||
"tslint-microsoft-contrib",
|
||||
"../tslint.json"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-variable": true,
|
||||
"align": false,
|
||||
"array-type": [
|
||||
true,
|
||||
"array"
|
||||
],
|
||||
"arrow-parens": false,
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
false,
|
||||
"check-space"
|
||||
],
|
||||
"completed-docs": false,
|
||||
"await-promise": false,
|
||||
"curly": true,
|
||||
"eofline": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"function-name": false,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
4
|
||||
],
|
||||
"interface-name": [
|
||||
true,
|
||||
"always-prefix"
|
||||
],
|
||||
"jsdoc-format": true,
|
||||
"jsx-no-lambda": true,
|
||||
"jsx-no-bind": true,
|
||||
"jsx-no-multiline-js": false,
|
||||
"jsx-no-string-ref": true,
|
||||
"jsx-self-close": true,
|
||||
"jsx-wrap-multiline": true,
|
||||
"label-position": true,
|
||||
"max-line-length": [
|
||||
false,
|
||||
140
|
||||
],
|
||||
"member-access": [
|
||||
true,
|
||||
"check-accessor"
|
||||
],
|
||||
"max-classes-per-file": false,
|
||||
"max-func-body-length": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"public-static-field",
|
||||
"protected-static-field",
|
||||
"private-static-field",
|
||||
"public-instance-field",
|
||||
"protected-instance-field",
|
||||
"public-static-method",
|
||||
"protected-static-method",
|
||||
"private-static-method",
|
||||
"public-instance-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"missing-jsdoc": false,
|
||||
"newline-before-return": false,
|
||||
"no-any": false,
|
||||
"no-arg": true,
|
||||
"no-bitwise": false,
|
||||
"no-consecutive-blank-lines": [
|
||||
true
|
||||
],
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-empty": true,
|
||||
"no-eval": true,
|
||||
"no-for-in": false,
|
||||
"no-implicit-dependencies": false,
|
||||
"no-import-side-effect": false,
|
||||
"no-namespace": false,
|
||||
"no-null-keyword": false,
|
||||
"no-object-literal-type-assertion": false,
|
||||
"no-parameter-properties": false,
|
||||
"no-relative-imports": true,
|
||||
"no-reserved-keywords": false,
|
||||
"no-shadowed-variable": false,
|
||||
"no-single-line-block-comment": false,
|
||||
"no-string-literal": false,
|
||||
"no-submodule-imports": false,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unused-expression": false,
|
||||
"no-unused-variable": true,
|
||||
"no-unused-method": true,
|
||||
"no-use-before-declare": false,
|
||||
"no-var-requires": false,
|
||||
"object-literal-shorthand": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-finally",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"one-variable-per-declaration": [
|
||||
true,
|
||||
"ignore-for-loop"
|
||||
],
|
||||
"ordered-imports": false,
|
||||
"prefer-const": true,
|
||||
"prefer-method-signature": false,
|
||||
"prefer-type-cast": false,
|
||||
"quotemark": [
|
||||
true,
|
||||
"double"
|
||||
],
|
||||
"radix": true,
|
||||
"react-a11y-anchors": false,
|
||||
"react-a11y-aria-unsupported-elements": false,
|
||||
"react-a11y-event-has-role": false,
|
||||
"react-a11y-image-button-has-alt": false,
|
||||
"react-a11y-img-has-alt": false,
|
||||
"react-a11y-lang": false,
|
||||
"react-a11y-meta": false,
|
||||
"react-a11y-props": false,
|
||||
"react-a11y-proptypes": false,
|
||||
"react-a11y-role": false,
|
||||
"react-a11y-role-has-required-aria-props": false,
|
||||
"react-a11y-role-supports-aria-props": false,
|
||||
"react-a11y-tabindex-no-positive": false,
|
||||
"react-a11y-titles": false,
|
||||
"react-anchor-blank-noopener": false,
|
||||
"react-iframe-missing-sandbox": false,
|
||||
"react-no-dangerous-html": false,
|
||||
"react-this-binding-issue": false,
|
||||
"react-tsx-curly-spacing": false,
|
||||
"react-unused-props-and-state": false,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"switch-default": false,
|
||||
"trailing-comma": [
|
||||
true,
|
||||
{
|
||||
"multiline": "never",
|
||||
"singleline": "never"
|
||||
}
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef": [
|
||||
false,
|
||||
"variable-declaration"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
],
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "rollup-extension",
|
||||
"globalDependencies": {
|
||||
"tfs": "npm:vss-web-extension-sdk/typings/tfs.d.ts",
|
||||
"vss": "npm:vss-web-extension-sdk/typings/vss.d.ts"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"manifestVersion": 1,
|
||||
"id": "workitem-feature-timeline-extension",
|
||||
"version": "0.0.162",
|
||||
"name": "Feature timeline",
|
||||
"description": "Feature timeline of your in-progress features.",
|
||||
"publisher": "ms-devlabs",
|
||||
"targets": [{
|
||||
"id": "Microsoft.VisualStudio.Services.Cloud"
|
||||
},
|
||||
{
|
||||
"id": "Microsoft.TeamFoundation.Server",
|
||||
"version": "[15.2,)"
|
||||
}],
|
||||
"icons": {
|
||||
"default": "dist/images/icon.png"
|
||||
},
|
||||
"tags": ["work item", "timeline", "feature"],
|
||||
"content": {
|
||||
"details": {
|
||||
"path": "dist/details.md"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"uri": "https://github.com/microsoft/featuretimeline"
|
||||
},
|
||||
"scopes": [
|
||||
"vso.work",
|
||||
"vso.work_write"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "dist",
|
||||
"addressable": true
|
||||
}
|
||||
],
|
||||
"categories": ["Plan and track"],
|
||||
"contributions": [{
|
||||
"id": "workitem-feature-timeline",
|
||||
"type": "ms.vss-web.tab",
|
||||
"description": "Feature timeline of your in-progress features.",
|
||||
"targets": [
|
||||
"ms.vss-work-web.product-backlog-tabs"
|
||||
],
|
||||
"properties": {
|
||||
"name": "Feature Timeline",
|
||||
"order": 99,
|
||||
"uri": "dist/index.html",
|
||||
"dynamic": true
|
||||
}
|
||||
}]
|
||||
}
|