This commit is contained in:
Navneet Gupta 2018-07-28 23:14:54 -07:00
Родитель 86342174ad
Коммит cc43e45aaa
211 изменённых файлов: 43499 добавлений и 36879 удалений

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

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

@ -0,0 +1,34 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.autoSave": "afterDelay",
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/.github": true,
"**/dist": true,
"**/typings": true,
"**/.gitignore": true,
"**/*vsix": true,
"**/.vscode": true,
"**/*code-workspace*": true,
"**/package-lock*": true,
"**/LICENSE": true,
"**/docs": true,
"**/images": true,
"**/configs": true,
"**/scripts": true,
"**/*.md": true,
//"**/*.json": true,
//"**/*config.js": true,
}
}
}

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

@ -0,0 +1,34 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.autoSave": "afterDelay",
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/.github": true,
"**/dist": true,
"**/typings": true,
"**/.gitignore": true,
"**/*vsix": true,
"**/.vscode": true,
"**/*code-workspace*": true,
"**/package-lock*": true,
"**/LICENSE": true,
"**/docs": true,
"**/images": true,
"**/configs": true,
"**/scripts": true,
"**/*.md": true,
//"**/*.json": true,
//"**/*config.js": true,
}
}
}

8147
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -29,6 +29,7 @@
"dependencies": {
"es6-promise": "^4.2.4",
"immer": "^1.3.1",
"npm": "^6.2.0",
"office-ui-fabric-react": "^5.117.0",
"rc-input-number": "^4.0.12",
"react": "^16.4.1",
@ -37,7 +38,7 @@
"react-dom": "^16.4.1",
"react-redux": "^5.0.7",
"react-splitter-layout": "^3.0.0",
"redux": "^3.7.2",
"redux": "^4.0.0",
"redux-saga": "^0.16.0",
"reselect": "^3.0.1",
"vss-web-extension-sdk": "^5.134.0"
@ -56,7 +57,6 @@
"node-sass": "^4.9.2",
"prettier": "^1.13.7",
"prettier-webpack-plugin": "^1.0.0",
"redux-devtools": "^3.4.1",
"rimraf": "^2.6.1",
"sass-loader": "^6.0.7",
"source-map-loader": "^0.2.3",

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

@ -1,52 +1,51 @@
import { DropTarget } from 'react-dnd';
import { IIterationSahdowProps, IterationShadow } from './IterationShadow';
import React = require('react');
import { IWorkItemRendererProps } from './WorkItem/WorkItemRenderer';
import { IWorkItemListItemProps } from './WorkItem/WorkItemListItem';
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;
}
let draggedItem = monitor.getItem();
if (draggedItem["dimension"]) {
const item = draggedItem as IWorkItemRendererProps;
dropTargetProps.changeIteration(item.id, dropTargetProps.teamIteration, draggedItem.allowOverride);
} else {
const item = draggedItem as IWorkItemListItemProps;
dropTargetProps.markInProgress(item.id, dropTargetProps.teamIteration, item.inProgressState);
}
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()
};
}
import { DropTarget } from 'react-dnd';
import { IIterationSahdowProps, IterationShadow } from './IterationShadow';
import { IWorkItemListItemProps } from './WorkItem/WorkItemListItem';
import { IWorkItemRendererProps } from './WorkItem/WorkItemRenderer';
import React = require('react');
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;
}
let draggedItem = monitor.getItem();
if (draggedItem["dimension"]) {
const item = draggedItem as IWorkItemRendererProps;
dropTargetProps.changeIteration(item.id, dropTargetProps.teamIteration, draggedItem.allowOverrideIteration);
} else {
const item = draggedItem as IWorkItemListItemProps;
dropTargetProps.markInProgress(item.id, dropTargetProps.teamIteration, item.inProgressState);
}
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);

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

@ -1,11 +1,11 @@
.info-icon {
margin-left: 5px;
color: transparent !important;
cursor: pointer;
display: flex;
align-items: center;
}
.info-icon:hover {
color: grey !important;
}
.info-icon {
margin-left: 5px;
color: transparent !important;
cursor: pointer;
display: flex;
align-items: center;
}
.info-icon:hover {
color: grey !important;
}

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

@ -1,18 +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)}>
&nbsp;
</div>
);
}
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)}>
&nbsp;
</div>
);
}
}

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

@ -1,47 +1,47 @@
.iteration {
display: flex;
flex-direction: column;
background: #f4f4f4;
padding: 0px 10px;
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: 500;
font-size: 12px;
padding: 2px;
}
.dates {
color: #666666;
font-size: 11px;
padding: 2px;
}
}
@mixin marker {
margin-left: 3px;
padding-left: 8px;
padding-right: 8px;
padding-top: 1px;
padding-bottom: 2px;
color: rgb(255, 255, 255);
font-size: 10px;
line-height: 1.5;
width: 100px;
min-width: 60px;
text-align: center;
border-radius: 5px;
}
.current-sprint-marker {
@include marker();
background-color: rgb(16, 110, 190);
}
.unplanned-sprint-marker {
@include marker();
background-color: orangered;
.iteration {
display: flex;
flex-direction: column;
background: #f4f4f4;
padding: 0px 10px;
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: 500;
font-size: 12px;
padding: 2px;
}
.dates {
color: #666666;
font-size: 11px;
padding: 2px;
}
}
@mixin marker {
margin-left: 3px;
padding-left: 8px;
padding-right: 8px;
padding-top: 1px;
padding-bottom: 2px;
color: rgb(255, 255, 255);
font-size: 10px;
line-height: 1.5;
width: 100px;
min-width: 60px;
text-align: center;
border-radius: 5px;
}
.current-sprint-marker {
@include marker();
background-color: rgb(16, 110, 190);
}
.unplanned-sprint-marker {
@include marker();
background-color: orangered;
}

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

@ -1,60 +1,60 @@
import "./IterationRenderer.scss";
import * as React from "react";
import { TeamSettingsIteration } from "TFS/Work/Contracts";
import { isCurrentIteration } from "../../redux/helpers/iterationComparer";
export interface IIterationRendererProps {
iteration: TeamSettingsIteration;
teamIterations: 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,
teamIterations,
} = this.props;
const startDate = iteration.attributes && iteration.attributes["startDate"] ? getMMDD(new Date(iteration.attributes["startDate"])) : null;
const endDate = iteration.attributes && 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>
);
}
const isBacklogIteration = !teamIterations.some(ti => ti.id === iteration.id);
let marker = null;
if (isBacklogIteration) {
marker = (
<span className="unplanned-sprint-marker">Backlog</span>
);
} else if (isCurrentIteration(teamIterations, iteration)) {
marker = (
<span className="current-sprint-marker">Current</span>
);
}
const name = iteration && iteration.name || "";
return (
<div className="iteration">
<div className="iterationname">
<span>{name}</span>
{marker}
</div>
{dates}
</div>
);
}
import "./IterationRenderer.scss";
import * as React from "react";
import { TeamSettingsIteration } from "TFS/Work/Contracts";
import { isCurrentIteration } from "../../redux/Helpers/iterationComparer";
export interface IIterationRendererProps {
iteration: TeamSettingsIteration;
teamIterations: 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,
teamIterations,
} = this.props;
const startDate = iteration.attributes && iteration.attributes["startDate"] ? getMMDD(new Date(iteration.attributes["startDate"])) : null;
const endDate = iteration.attributes && 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>
);
}
const isBacklogIteration = !teamIterations.some(ti => ti.id === iteration.id);
let marker = null;
if (isBacklogIteration) {
marker = (
<span className="unplanned-sprint-marker">Backlog</span>
);
} else if (isCurrentIteration(teamIterations, iteration)) {
marker = (
<span className="current-sprint-marker">Current</span>
);
}
const name = iteration && iteration.name || "";
return (
<div className="iteration">
<div className="iterationname">
<span>{name}</span>
{marker}
</div>
{dates}
</div>
);
}
}

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

@ -1,9 +1,9 @@
.columnshadow {
margin-top: 2px;
background: #f4f4f4;
min-height: 36px;
}
.highlight {
border: 1px solid yellowgreen;
.columnshadow {
margin-top: 2px;
background: #f4f4f4;
min-height: 36px;
}
.highlight {
border: 1px solid yellowgreen;
}

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

@ -1,71 +1,71 @@
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;
markInProgress: (id: number, teamIteration: TeamSettingsIteration, state: string) => 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
})
}
}
import './IterationShadow.scss';
import * as React from 'react';
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
import { getRowColumnStyle } from '../../redux/Helpers/gridhelper';
import { IGridIteration } from '../../redux/Contracts/GridViewContracts';
export interface IIterationSahdowProps extends IGridIteration {
isOverrideIterationInProgress: boolean;
onOverrideIterationOver: (iteration: string) => void;
changeIteration: (id: number, teamIteration: TeamSettingsIteration, override: boolean) => void;
markInProgress: (id: number, teamIteration: TeamSettingsIteration, state: string) => 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,25 @@
.successor-predeccessor {
margin-left: 5px;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 25px;
}
.work-item-links-list-callout {
padding: 10px;
}
.callout-title {
font-weight: bolder;
}
.highlight-icon {
background: yellow;
border-color: yellowgreen;
}
.team-field-value {
font-weight: bolder;
margin-bottom: 5px;
}

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

@ -0,0 +1,108 @@
import { css } from '@uifabric/utilities/lib';
import * as React from 'react';
import { WorkItem } from 'TFS/WorkItemTracking/Contracts';
import './PredecessorSuccessorIcon.scss';
import { Callout } from 'office-ui-fabric-react/lib/Callout';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { SimpleWorkItem } from '../WorkItem/SimpleWorkItem';
export interface IPredecessorSuccessorIconProps {
id: number;
workItems: WorkItem[];
hasSuccessors: boolean;
isHighlighted: boolean;
teamFieldName: string;
onShowWorkItem: (id: number) => void;
onHighlightDependencies: (id: number, highlightSuccessor: boolean) => void;
onDismissDependencies: () => void;
}
interface IPredecessorSuccessorIconState {
isCalloutVisible: boolean;
}
export class PredecessorSuccessorIcon extends React.Component<IPredecessorSuccessorIconProps, IPredecessorSuccessorIconState> {
private _containerDiv: HTMLDivElement = null;
public constructor(props, context) {
super(props, context);
this.state = {
isCalloutVisible: false
}
}
public render() {
const icon = this.props.hasSuccessors ? "bowtie-navigate-forward-circle" : "bowtie-navigate-back-circle";
const highlight = this.props.isHighlighted ? "highlight-icon" : "";
return (
<div className={css("bowtie-icon", icon, "successor-predeccessor", highlight)} onClick={this._toggleCallout} ref={div => (this._containerDiv = div)}>
{this._renderCallout()}
</div>
);
}
private _renderCallout() {
if (!this.state || !this.state.isCalloutVisible || !this._containerDiv) {
return null;
}
const filteredItems = this.props.workItems
.filter(w => !!w);
const itemsByTeamField = filteredItems.reduce((map, w) => {
const value = this._getSimpleTeamFieldName(w.fields[this.props.teamFieldName]);
if (!map[value]) {
map[value] = [];
}
map[value].push(w);
return map;
}, {});
let items = [];
Object.keys(itemsByTeamField)
.sort()
.forEach(teamField => {
items.push(<div className="team-field-value">{teamField}</div>)
const workItems = itemsByTeamField[teamField];
items = items.concat(workItems.map(this._renderWorkItem))
});
const label = <Label className="callout-title">{this.props.hasSuccessors ? "Successors" : "Predecessors"}</Label>
return (
<Callout
className="work-item-links-list-callout"
target={this._containerDiv}
isBeakVisible={true}
onDismiss={this._toggleCallout}
>
{label}
{items}
</Callout>
)
}
private _getSimpleTeamFieldName = (teamField) => {
teamField = teamField || "";
const parts = teamField.split("\\");
return parts[parts.length - 1];
}
private _toggleCallout = () => {
const isCalloutVisible = !this.state.isCalloutVisible;
if (isCalloutVisible) {
this.props.onHighlightDependencies(this.props.id, this.props.hasSuccessors);
} else {
this.props.onDismissDependencies();
}
this.setState({
isCalloutVisible,
});
}
private _renderWorkItem = (workItem: WorkItem) => {
return (
<SimpleWorkItem
workItem={workItem}
onShowWorkItem={this.props.onShowWorkItem}
/>
);
}
}

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

@ -1,28 +1,28 @@
.progress-indicator-container {
display: flex;
align-items: center;
cursor: pointer;
margin-left: 10px;
}
.progress-indicator-tooltip {
flex: 1;
}
.progress-details-parts {
width: 70px;
display: flex;
height: 10px;
background: lightgrey;
}
.progress-completed {
background: limegreen;
}
.progress-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: 5px;
.progress-indicator-container {
display: flex;
align-items: center;
cursor: pointer;
margin-left: 10px;
}
.progress-indicator-tooltip {
flex: 1;
}
.progress-details-parts {
width: 70px;
display: flex;
height: 10px;
background: lightgrey;
}
.progress-completed {
background: limegreen;
}
.progress-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: 5px;
}

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

@ -1,36 +1,36 @@
import './ProgressDetails.scss';
import * as React from 'react';
import { IProgressIndicator } from '../../../redux/selectors/gridViewSelector';
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
export interface IProgressIndicatorProps extends IProgressIndicator {
onClick: () => void;
}
export class ProgressDetails extends React.Component<IProgressIndicatorProps, {}> {
public render() {
const {
total,
completed,
onClick
} = this.props;
if (total <= 0) {
return null;
}
const style = {};
style['width'] = `${(completed * 100 / total)}%`;
const progressText = `${completed}/${total}`;
return (
<TooltipHost content={progressText} className="progress-indicator-tooltip">
<div className="progress-indicator-container" onClick={onClick}>
<div className="progress-details-parts">
<div className="progress-completed" style={style} />
</div>
<div className="progress-text"> {progressText}</div>
</div>
</TooltipHost>
)
}
import './ProgressDetails.scss';
import * as React from 'react';
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
import { IProgressIndicator } from '../../../redux/Contracts/GridViewContracts';
export interface IProgressIndicatorProps extends IProgressIndicator {
onClick: () => void;
}
export class ProgressDetails extends React.Component<IProgressIndicatorProps, {}> {
public render() {
const {
total,
completed,
onClick
} = this.props;
if (total <= 0) {
return null;
}
const style = {};
style['width'] = `${(completed * 100 / total)}%`;
const progressText = `${completed}/${total}`;
return (
<TooltipHost content={progressText} className="progress-indicator-tooltip">
<div className="progress-indicator-container" onClick={onClick}>
<div className="progress-details-parts">
<div className="progress-completed" style={style} />
</div>
<div className="progress-text"> {progressText}</div>
</div>
</TooltipHost>
)
}
}

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

@ -1,20 +1,20 @@
.state-container {
display: flex;
flex-direction: row;
align-items: center;
}
.state-indicator {
height: 6px;
width: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
padding: 5px;
margin: 2px;
cursor: pointer;
}
.state-name {
margin-left: 5px;
.state-container {
display: flex;
flex-direction: row;
align-items: center;
}
.state-indicator {
height: 6px;
width: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
padding: 5px;
margin: 2px;
cursor: pointer;
}
.state-name {
margin-left: 5px;
}

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

@ -1,27 +1,27 @@
import './State.scss';
import * as React from 'react';
import { WorkItemStateColor } from 'TFS/WorkItemTracking/Contracts';
export interface IStateProps {
workItemStateColor: WorkItemStateColor;
}
export class State extends React.Component<IStateProps, {}> {
public render() {
const {
workItemStateColor
} = this.props;
const stateColorStyle = {};
const color = "#" + (workItemStateColor.color.length > 6 ? workItemStateColor.color.substr(2) : workItemStateColor.color)
stateColorStyle['background'] = color;
return (
<div className="state-container">
<div className="state-indicator" style={stateColorStyle} />
<div className="state-name">{workItemStateColor.name}</div>
</div>
)
}
import './State.scss';
import * as React from 'react';
import { WorkItemStateColor } from 'TFS/WorkItemTracking/Contracts';
export interface IStateProps {
workItemStateColor: WorkItemStateColor;
}
export class State extends React.Component<IStateProps, {}> {
public render() {
const {
workItemStateColor
} = this.props;
const stateColorStyle = {};
const color = "#" + (workItemStateColor.color.length > 6 ? workItemStateColor.color.substr(2) : workItemStateColor.color)
stateColorStyle['background'] = color;
return (
<div className="state-container">
<div className="state-indicator" style={stateColorStyle} />
<div className="state-name">{workItemStateColor.name}</div>
</div>
)
}
}

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

@ -0,0 +1,8 @@
.team-field-card{
border: solid 1px lightgrey;
padding: 5px;
font-size: 15px;
text-align: center;
background-color: #f4f4f4;
margin-bottom: 5px;
}

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

@ -0,0 +1,27 @@
import "./TeamFieldCard.scss";
import * as React from "react";
import { IDimension } from "../../../redux/Contracts/types";
import { getRowColumnStyle } from "../../../redux/Helpers/gridhelper";
import { Label } from 'office-ui-fabric-react/lib/Label';
export interface ITeamFieldCardProps {
dimension: IDimension;
teamField: string;
}
export class TeamFieldCard extends React.Component<ITeamFieldCardProps, {}> {
public render() {
const {
dimension,
teamField
} = this.props;
const style = getRowColumnStyle(dimension);
return (
<div
className="team-field-card"
style={style}
>
<Label>{teamField}</Label>
</div>
);
}
}

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

@ -0,0 +1,17 @@
.team-field-header {
display: flex;
flex-direction: column;
background: #f4f4f4;
padding: 0px 10px;
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%;
text-align: center;
.team-field-text {
color: #212121;
font-weight: 500;
font-size: 12px;
padding: 2px;
}
}

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

@ -0,0 +1,26 @@
import "./TeamFieldHeader.scss";
import * as React from "react";
import { IDimension } from "../../../redux/Contracts/types";
import { getRowColumnStyle } from "../../../redux/Helpers/gridhelper";
export interface ITeamFieldHeaderProps {
dimension: IDimension
}
export class TeamFieldHeader extends React.Component<ITeamFieldHeaderProps, {}> {
public render(): JSX.Element {
const {
dimension,
} = this.props;
const style = getRowColumnStyle(dimension);
return (
<div className="team-field-header" style={style}>
<div className="team-field-text">
<span>{"Area Path"}</span>
</div>
</div>
);
}
}

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

@ -0,0 +1,16 @@
import "./WorkItemRenderer.scss";
import * as React from 'react';
import { getRowColumnStyle } from '../../../redux/Helpers/gridhelper';
import { IDimension } from "../../../redux/Contracts/types";
export class ChildRowsSeparator extends React.Component<IDimension, {}> {
public render() {
const style = getRowColumnStyle(this.props);
return (
<div className="child-rows-separator" style={style}>
<div className="title"></div>
</div>
);
}
}

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

@ -1,28 +1,28 @@
import { DragSource } from 'react-dnd';
import * as React from 'react';
import { IWorkItemListItemProps, WorkItemListItem } from './WorkItemListItem';
class DraggableHelper extends React.Component<IWorkItemListItemProps, {}> {
public render() {
return <WorkItemListItem {...this.props} />;
}
}
const WorkItemSource = {
beginDrag(props: IWorkItemListItemProps) {
return props;
}
}
/**
* Specifies the props to inject into your component.
*/
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
};
}
import { DragSource } from 'react-dnd';
import * as React from 'react';
import { IWorkItemListItemProps, WorkItemListItem } from './WorkItemListItem';
class DraggableHelper extends React.Component<IWorkItemListItemProps, {}> {
public render() {
return <WorkItemListItem {...this.props} />;
}
}
const WorkItemSource = {
beginDrag(props: IWorkItemListItemProps) {
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);

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

@ -1,28 +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);
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 const DraggableWorkItemRenderer = DragSource("WorkItem", WorkItemSource, collect)(DraggableHelper);

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

@ -0,0 +1,8 @@
.work-item-link-container {
display: flex;
flex-direction: row;
margin: 2px;
}
.work-item-link-id {
margin-right: 5px;
}

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

@ -0,0 +1,24 @@
import { Link } from "office-ui-fabric-react/lib/Link";
import * as React from "react";
import { WorkItem } from "TFS/WorkItemTracking/Contracts";
import "./SimpleWorkItem.scss";
export interface ISimpleWorkItemProps {
workItem: WorkItem;
onShowWorkItem: (id: number) => void;
}
export class SimpleWorkItem extends React.Component<ISimpleWorkItemProps, {}>{
public render() {
const {
workItem
} = this.props;
return (
<div className="work-item-link-container">
<div className="work-item-link-id">{workItem.id}</div>
<Link className="work-item-link-title" href="#" onClick={() => this.props.onShowWorkItem(workItem.id)}>{workItem.fields["System.Title"]}</Link>
</div>
);
}
}

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

@ -0,0 +1,16 @@
import "./WorkItemRenderer.scss";
import * as React from 'react';
import { getRowColumnStyle } from '../../../redux/Helpers/gridhelper';
import { IDimension } from "../../../redux/Contracts/types";
export class ChildRowsSeparator extends React.Component<IDimension, {}> {
public render() {
const style = getRowColumnStyle(this.props);
return (
<div className="child-rows-separator" style={style}>
<div className="title"></div>
</div>
);
}
}

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

@ -1,31 +1,31 @@
@mixin default {
display: flex;
flex-direction: row;
color: black;
text-align: center;
border-style: solid;
border-width: 0.5px;
border-left-width: 5px;
margin: 4px;
font-size: 15px;
padding: 2px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
border-radius: 5px;
}
.work-item-list-item {
@include default;
.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;
font-weight: 400;
}
@mixin default {
display: flex;
flex-direction: row;
color: black;
text-align: center;
border-style: solid;
border-width: 0.5px;
border-left-width: 5px;
margin: 4px;
font-size: 15px;
padding: 2px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
border-radius: 5px;
}
.work-item-list-item {
@include default;
.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;
font-weight: 400;
}
}

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

@ -1,59 +1,59 @@
import './WorkItemListItem.scss';
import * as React from 'react';
import { TooltipHost, TooltipOverflowMode } from 'office-ui-fabric-react/lib/Tooltip';
import { hexToRgb } from '../colorhelper';
export interface IWorkItemListItemProps {
id: number;
title: string;
color: string;
inProgressState: string;
onClick: (id: number) => void;
isDragging?: boolean;
connectDragSource?: (element: JSX.Element) => JSX.Element;
}
export class WorkItemListItem extends React.Component<IWorkItemListItemProps, {}> {
public render() {
const {
id,
title,
onClick,
isDragging,
} = this.props;
let style = {};
if (isDragging) {
style['border-color'] = hexToRgb(this.props.color, 0.1);
} else {
style['border-color'] = hexToRgb(this.props.color, 0.8);
}
const className = "work-item-list-item";
const item = (
<div style={style}
className={className}
>
<div
className="title-contents"
onClick={() => onClick(id)}
>
<TooltipHost
content={title}
overflowMode={TooltipOverflowMode.Parent}
>
{title}
</TooltipHost>
</div>
</div>
);
const { connectDragSource } = this.props;
return connectDragSource(item);
}
import { TooltipHost, TooltipOverflowMode } from 'office-ui-fabric-react/lib/Tooltip';
import * as React from 'react';
import { hexToRgb } from '../colorhelper';
import './WorkItemListItem.scss';
export interface IWorkItemListItemProps {
id: number;
title: string;
color: string;
inProgressState: string;
onClick: (id: number) => void;
isDragging?: boolean;
connectDragSource?: (element: JSX.Element) => JSX.Element;
}
export class WorkItemListItem extends React.Component<IWorkItemListItemProps, {}> {
public render() {
const {
id,
title,
onClick,
isDragging,
} = this.props;
let style = {};
if (isDragging) {
style['border-color'] = hexToRgb(this.props.color, 0.1);
} else {
style['border-color'] = hexToRgb(this.props.color, 0.8);
}
const className = "work-item-list-item";
const item = (
<div style={style}
className={className}
>
<div
className="title-contents"
onClick={() => onClick(id)}
>
<TooltipHost
content={title}
overflowMode={TooltipOverflowMode.Parent}
>
{title}
</TooltipHost>
</div>
</div>
);
const { connectDragSource } = this.props;
return connectDragSource(item);
}
}

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

@ -1,143 +1,143 @@
@mixin textOverflow {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
@mixin rootContainer {
display: flex;
flex-direction: row;
background: white;
margin-top: 10px;
padding: 2px;
text-align: center;
font-size: 15px;
border-radius: 8px;
border: 1px solid;
border-left: 5px solid;
justify-content: space-between;
overflow: hidden;
}
.work-item-renderer {
@include rootContainer();
}
.root-work-item-renderer {
@include rootContainer();
margin-top: 0px;
min-height: 46px;
height: calc(100% - 5px);
position: sticky;
left: 0;
}
.work-item-detail-rows {
flex: 1;
display: flex;
flex-direction: column;
@include textOverflow();
}
.work-item-detail-row {
display: flex;
flex-direction: row;
align-items: center;
@include textOverflow();
}
.work-item-row {
display: flex;
}
.work-item-details-container {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
width: 100%;
@include textOverflow();
}
.work-item-details-container:hover+.info-icon {
color: grey !important;
}
.work-item-details-without-infoicon {
width: calc(100% - 10px);
}
.work-item-details-with-infoicon {
width: calc(100% - 30px);
}
@mixin work-item-iteration-indicator {
cursor: pointer;
background: lightgray;
opacity: 0.7;
color: black;
padding-left: 5px;
padding-right: 5px;
padding-bottom: 2px;
font-size: 12px;
}
.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;
}
.work-item-iteration-override-handle {
width: 5px;
background: transparent;
cursor: ew-resize;
}
.title-contents {
cursor: pointer;
padding-left: 2px;
padding-right: 2px;
padding-bottom: 2px;
flex-grow: 1 0 auto;
font-weight: 400;
@include textOverflow();
}
.root-work-item-renderer {
.title-contents {
white-space: normal;
}
}
.work-item-shadow {
@include rootContainer;
border: transparent;
background: transparent;
color: white;
}
.work-item-gap {
margin-top: 10px;
border: 1px solid lightgrey;
}
.secondary-row {
font-size: 12px;
margin-top: 5px;
margin-bottom: 5px;
}
.work-item-warning {
margin-left: 5px;
margin-top: 5px;
color: orange;
font-weight: bold;
cursor: pointer;
@mixin textOverflow {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
@mixin rootContainer {
display: flex;
flex-direction: row;
background: white;
margin-top: 10px;
padding: 2px;
text-align: center;
font-size: 15px;
border-radius: 8px;
border: 1px solid;
border-left: 5px solid;
justify-content: space-between;
overflow: hidden;
}
.work-item-renderer {
@include rootContainer();
}
.root-work-item-renderer {
@include rootContainer();
margin-top: 0px;
min-height: 46px;
height: calc(100% - 5px);
position: sticky;
left: 0;
}
.work-item-detail-rows {
flex: 1;
display: flex;
flex-direction: column;
@include textOverflow();
}
.work-item-detail-row {
display: flex;
flex-direction: row;
align-items: center;
@include textOverflow();
}
.work-item-row {
display: flex;
}
.work-item-details-container {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
width: 100%;
@include textOverflow();
}
.work-item-details-container:hover+.info-icon {
color: grey !important;
}
.work-item-details-without-infoicon {
width: calc(100% - 10px);
}
.work-item-details-with-infoicon {
width: calc(100% - 30px);
}
@mixin work-item-iteration-indicator {
cursor: pointer;
background: lightgray;
opacity: 0.7;
color: black;
padding-left: 5px;
padding-right: 5px;
padding-bottom: 2px;
font-size: 12px;
}
.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;
}
.work-item-iteration-override-handle {
width: 5px;
background: transparent;
cursor: ew-resize;
}
.title-contents {
cursor: pointer;
padding-left: 2px;
padding-right: 2px;
padding-bottom: 2px;
flex-grow: 1 0 auto;
font-weight: 400;
@include textOverflow();
}
.root-work-item-renderer {
.title-contents {
white-space: normal;
}
}
.work-item-shadow {
@include rootContainer;
border: transparent;
background: transparent;
color: white;
}
.child-rows-separator {
margin-top: 10px;
border: 1px solid lightgrey;
}
.secondary-row {
font-size: 12px;
margin-top: 5px;
margin-bottom: 5px;
}
.work-item-warning {
margin-left: 5px;
margin-top: 5px;
color: orange;
font-weight: bold;
cursor: pointer;
}

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

@ -1,374 +1,421 @@
import './WorkItemRenderer.scss';
import * as React from 'react';
import { InfoIcon } from '../InfoIcon/InfoIcon';
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';
import { hexToRgb } from '../colorhelper';
import { ProgressDetails } from '../ProgressDetails/ProgressDetails';
import { IProgressIndicator } from '../../../redux/selectors/gridViewSelector';
import { WorkItemStateColor } from 'TFS/WorkItemTracking/Contracts';
import { State } from '../State/State';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { ISettingsState, ProgressTrackingCriteria, IIterationDuration, IWorkItemOverrideIteration } from '../../../redux/store/types';
export interface IWorkItemRendererProps {
id: number;
title: string;
workItemStateColor: WorkItemStateColor;
color: string;
isRoot: boolean;
isSubGrid: boolean;
showInfoIcon: boolean;
allowOverride: boolean;
iterationDuration: IIterationDuration;
dimension: IDimension;
crop: CropWorkItem;
progressIndicator: IProgressIndicator;
settingsState: ISettingsState;
efforts: number;
childrernWithNoEfforts: number;
isComplete: number;
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,
showInfoIcon,
allowOverride,
isDragging,
crop,
iterationDuration,
progressIndicator,
workItemStateColor,
settingsState,
childrernWithNoEfforts,
efforts,
isSubGrid,
isComplete
} = 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['border-color'] = hexToRgb(this.props.color, 0.1);
} else if(isComplete){
style['border-color'] = hexToRgb(this.props.color, 0.3);
}
else {
style['border-color'] = hexToRgb(this.props.color, 0.7);
}
const rendererClass = isRoot ? "root-work-item-renderer" : "work-item-renderer";
let canOverrideLeft = allowOverride;
let canOverrideRight = allowOverride;
let leftCropped = false;
let rightCropped = false;
switch (crop) {
case CropWorkItem.Left: {
canOverrideLeft = false;
leftCropped = true;
break;
}
case CropWorkItem.Right: {
canOverrideRight = false;
rightCropped = true;
break;
}
case CropWorkItem.Both: {
canOverrideLeft = false;
canOverrideRight = false;
leftCropped = true;
rightCropped = true;
break;
}
}
const infoIcon = showInfoIcon ? <InfoIcon id={id} onClick={id => showDetails(id)} /> : null;
const additionalDetailsContainer = infoIcon ? "work-item-details-with-infoicon" : "work-item-details-without-infoicon";
let leftHandle = null;
let rightHandle = null;
if (!isRoot && allowOverride) {
leftHandle = canOverrideLeft && (
<div
className="work-item-iteration-override-handle"
onMouseDown={this._leftMouseDown}
onMouseUp={this._mouseUp}
/>
);
rightHandle = canOverrideRight && (
<div
className="work-item-iteration-override-handle"
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>
);
}
let secondRow = null;
if (settingsState.showWorkItemDetails) {
let stateIndicator = null;
if (workItemStateColor && !isRoot) {
stateIndicator = <State workItemStateColor={workItemStateColor} />;
}
let warning = null;
let warningMessages = [];
if (iterationDuration.childrenAreOutofBounds) {
warningMessages.push("Some user stories for this feature are outside the bounds of start or end iteration of this feature.");
}
if (settingsState.progressTrackingCriteria === ProgressTrackingCriteria.EffortsField && childrernWithNoEfforts > 0) {
warningMessages.push("Some user stories for this feature do not have story points set.");
}
if (isSubGrid && efforts === 0 && settingsState.progressTrackingCriteria === ProgressTrackingCriteria.EffortsField) {
warningMessages.push("Story points are not set.")
}
if (warningMessages.length > 0 && !isRoot) {
const content = warningMessages.join(",");
warning = (
<TooltipHost
content={content}>
<Icon
iconName={'Warning'}
className="work-item-warning"
onClick={() => {
if (!isSubGrid)
{
showDetails(id);
} else {
onClick(id);
}
}
}
/>
</TooltipHost >
);
}
let progressDetails = null;
if (progressIndicator && !isRoot) {
progressDetails = (
<ProgressDetails
{...progressIndicator}
onClick={() => showDetails(id)}
/>);
}
secondRow = (
<div className="work-item-detail-row secondary-row">
{stateIndicator}
{progressDetails}
{warning}
</div>
);
}
const item = (
<div
className={rendererClass}
style={style}
ref={(e) => this._div = e}
>
{leftHandle}
<div className="work-item-detail-rows">
<div className="work-item-detail-row">
<div
className={css("work-item-details-container", additionalDetailsContainer)}
>
{startsFrom}
<div
className="title-contents"
onClick={() => onClick(id)}
>
<TooltipHost
content={title}
overflowMode={TooltipOverflowMode.Parent}
>
{title}
</TooltipHost>
</div>
{endsAt}
</div>
{infoIcon}
</div>
{secondRow}
</div>
{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
});
}
}
}
}
import './WorkItemRenderer.scss';
import * as React from 'react';
import { InfoIcon } from '../InfoIcon/InfoIcon';
import { getRowColumnStyle } from '../../../redux/Helpers/gridhelper';
import {
TooltipHost, TooltipOverflowMode
} from 'office-ui-fabric-react/lib/Tooltip';
import { css } from '@uifabric/utilities/lib/css';
import { hexToRgb } from '../colorhelper';
import { ProgressDetails } from '../ProgressDetails/ProgressDetails';
import { WorkItemStateColor, WorkItem } from 'TFS/WorkItemTracking/Contracts';
import { State } from '../State/State';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { IIterationDuration } from "../../../redux/Contracts/IIterationDuration";
import { IDimension, CropWorkItem } from '../../../redux/Contracts/types';
import { IWorkItemOverrideIteration } from '../../../redux/modules/OverrideIterations/overriddenIterationContracts';
import { IProgressIndicator } from '../../../redux/Contracts/GridViewContracts';
import { ISettingsState, ProgressTrackingCriteria } from '../../../redux/modules/SettingsState/SettingsStateContracts';
import { PredecessorSuccessorIcon } from '../PredecessorSuccessorIcon/PredecessorSuccessorIcon';
export interface IWorkItemRendererProps {
id: number;
title: string;
workItemStateColor: WorkItemStateColor;
color: string;
isRoot: boolean;
isSubGrid: boolean;
showInfoIcon: boolean;
allowOverrideIteration: boolean;
iterationDuration: IIterationDuration;
dimension: IDimension;
crop: CropWorkItem;
progressIndicator: IProgressIndicator;
settingsState: ISettingsState;
efforts: number;
childrernWithNoEfforts: number;
isComplete: boolean;
teamFieldName: string;
onClick: (id: number) => void;
showDetails: (id: number) => void;
overrideIterationStart: (payload: IWorkItemOverrideIteration) => void;
overrideIterationEnd: () => void;
isDragging?: boolean;
connectDragSource?: (element: JSX.Element) => JSX.Element;
predecessors: WorkItem[];
successors: WorkItem[];
highlightPredecessorIcon: boolean;
highlighteSuccessorIcon: boolean;
onHighlightDependencies: (id: number, highlightSuccessor: boolean) => void;
onDismissDependencies: () => void;
}
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,
showInfoIcon,
allowOverrideIteration,
isDragging,
crop,
iterationDuration,
progressIndicator,
workItemStateColor,
settingsState,
childrernWithNoEfforts,
efforts,
isSubGrid,
isComplete,
teamFieldName
} = 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['border-color'] = hexToRgb(this.props.color, 0.1);
} else if (isComplete) {
style['border-color'] = hexToRgb(this.props.color, 0.3);
}
else {
style['border-color'] = hexToRgb(this.props.color, 0.7);
}
const rendererClass = isRoot ? "root-work-item-renderer" : "work-item-renderer";
let canOverrideLeft = allowOverrideIteration;
let canOverrideRight = allowOverrideIteration;
let leftCropped = false;
let rightCropped = false;
switch (crop) {
case CropWorkItem.Left: {
canOverrideLeft = false;
leftCropped = true;
break;
}
case CropWorkItem.Right: {
canOverrideRight = false;
rightCropped = true;
break;
}
case CropWorkItem.Both: {
canOverrideLeft = false;
canOverrideRight = false;
leftCropped = true;
rightCropped = true;
break;
}
}
const infoIcon = showInfoIcon ? <InfoIcon id={id} onClick={id => showDetails(id)} /> : null;
const additionalDetailsContainer = infoIcon ? "work-item-details-with-infoicon" : "work-item-details-without-infoicon";
let leftHandle = null;
let rightHandle = null;
if (allowOverrideIteration) {
leftHandle = canOverrideLeft && (
<div
className="work-item-iteration-override-handle"
onMouseDown={this._leftMouseDown}
onMouseUp={this._mouseUp}
/>
);
rightHandle = canOverrideRight && (
<div
className="work-item-iteration-override-handle"
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>
);
}
let secondRow = null;
if (settingsState.showWorkItemDetails) {
let stateIndicator = null;
if (workItemStateColor && !isRoot) {
stateIndicator = <State workItemStateColor={workItemStateColor} />;
}
let warning = null;
let warningMessages = [];
if (iterationDuration.childrenAreOutofBounds) {
warningMessages.push("Some user stories for this feature are outside the bounds of start or end iteration of this feature.");
}
if (settingsState.progressTrackingCriteria === ProgressTrackingCriteria.EffortsField && childrernWithNoEfforts > 0) {
warningMessages.push("Some user stories for this feature do not have story points set.");
}
if (isSubGrid && efforts === 0 && settingsState.progressTrackingCriteria === ProgressTrackingCriteria.EffortsField) {
warningMessages.push("Story points are not set.")
}
if (warningMessages.length > 0 && !isRoot) {
const content = warningMessages.join(",");
warning = (
<TooltipHost
content={content}>
<Icon
iconName={'Warning'}
className="work-item-warning"
onClick={() => {
if (!isSubGrid) {
showDetails(id);
} else {
onClick(id);
}
}
}
/>
</TooltipHost >
);
}
let progressDetails = null;
if (progressIndicator && !isRoot) {
progressDetails = (
<ProgressDetails
{...progressIndicator}
onClick={() => showDetails(id)}
/>);
}
secondRow = (
<div className="work-item-detail-row secondary-row">
{stateIndicator}
{progressDetails}
{warning}
</div>
);
}
let predecessorsIcon = null;
let successorsIcon = null;
if (this.props.predecessors && this.props.predecessors.length > 0) {
predecessorsIcon = (
<PredecessorSuccessorIcon
id={this.props.id}
hasSuccessors={false}
workItems={this.props.predecessors}
onShowWorkItem={this.props.onClick}
onHighlightDependencies={this.props.onHighlightDependencies}
onDismissDependencies={this.props.onDismissDependencies}
isHighlighted={this.props.highlighteSuccessorIcon}
teamFieldName={teamFieldName}
/>
);
}
if (this.props.successors && this.props.successors.length > 0) {
successorsIcon = (
<PredecessorSuccessorIcon
id={this.props.id}
hasSuccessors={true}
workItems={this.props.successors}
onShowWorkItem={this.props.onClick}
onHighlightDependencies={this.props.onHighlightDependencies}
onDismissDependencies={this.props.onDismissDependencies}
isHighlighted={this.props.highlightPredecessorIcon}
teamFieldName={teamFieldName}
/>
);
}
const item = (
<div
className={rendererClass}
style={style}
ref={(e) => this._div = e}
>
{leftHandle}
<div className="work-item-detail-rows">
<div className="work-item-detail-row">
<div
className={css("work-item-details-container", additionalDetailsContainer)}
>
{predecessorsIcon}
{startsFrom}
<div
className="title-contents"
onClick={() => onClick(id)}
>
<TooltipHost
content={title}
overflowMode={TooltipOverflowMode.Parent}
>
{title}
</TooltipHost>
</div>
{endsAt}
</div>
{infoIcon}
{successorsIcon}
</div>
{secondRow}
</div>
{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
});
}
}
}
}

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

@ -1,28 +1,28 @@
import "./WorkItemRenderer.scss";
import * as React from 'react';
import { IDimension } from '../../../redux/types';
import { getRowColumnStyle } from '../gridhelper';
export interface IWorkItemShadowProps {
dimension: IDimension;
twoRows: boolean;
}
export class WorkItemShadow extends React.Component<IWorkItemShadowProps, {}> {
public render() {
const {
dimension,
twoRows
} = this.props;
const style = getRowColumnStyle(dimension);
if (twoRows) {
style['height'] = '52px';
}
return (
<div className="work-item-shadow" style={style}>
<div className="title">&nbsp;</div>
</div>
);
}
import "./WorkItemRenderer.scss";
import * as React from 'react';
import { getRowColumnStyle } from '../../../redux/Helpers/gridhelper';
import { IDimension } from "../../../redux/Contracts/types";
export interface IWorkItemShadowProps {
dimension: IDimension;
twoRows: boolean;
}
export class WorkItemShadow extends React.Component<IWorkItemShadowProps, {}> {
public render() {
const {
dimension,
twoRows
} = this.props;
const style = getRowColumnStyle(dimension);
if (twoRows) {
style['height'] = '52px';
}
return (
<div className="work-item-shadow" style={style}>
<div className="title">&nbsp;</div>
</div>
);
}
}

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

@ -1,4 +1,4 @@
.work-item-list-container{
display: flex;
flex-direction: column;
.work-item-list-container{
display: flex;
flex-direction: column;
}

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

@ -1,113 +1,112 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { IFeatureTimelineRawState } from '../../redux/store/types';
import { unplannedFeaturesSelector, planFeatureStateSelector } from '../../redux/selectors';
import { launchWorkItemForm } from '../../redux/store/workitems/actionCreators';
import { List } from 'office-ui-fabric-react/lib/List';
import DraggableWorkItemListItemRenderer from './WorkItem/DraggableWorkItemListItemRenderer';
import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { changePlanFeaturesFilter } from '../../redux/store/common/actioncreators';
export interface IWorkItemListItem {
id: number;
title: string;
color: string;
inProgressState: string;
order: number;
}
export interface IWorkItemListProps {
workItems: IWorkItemListItem[];
filter: string;
launchWorkItemForm: (id: number) => void;
onPlanFeaturesFilterChanged: (filter: string) => void;
}
export interface IWorkItemListState {
}
class WorkItemList extends React.Component<IWorkItemListProps, IWorkItemListState> {
public constructor(props: IWorkItemListProps) {
super(props);
this.state = {};
}
public render() {
if (this.props.workItems.length === 0 || !this.props.workItems) {
return (
<MessageBar>No new features to plan.</MessageBar>
);
}
const workItems = this._filteredItems(this.props.workItems);
let message = null;
if (workItems.length === 0) {
message = <MessageBar>Only top 100 Proposed features are available.</MessageBar>;
}
debugger;
return (
<div className="work-item-list-container">
<TextField
placeholder="Search Features"
onChanged={this._changeFilter}
value={this.props.filter}
/>
<List
items={workItems}
onRenderCell={this._onRenderCell}
/>
{message}
</div>
);
}
private _changeFilter = (text: string) => {
this.props.onPlanFeaturesFilterChanged(text);
}
private _filteredItems = (arg0: IWorkItemListItem[]): IWorkItemListItem[] => {
const {
filter
} = this.props;
if (!filter || !arg0) {
return arg0;
}
return arg0.filter(w => w.title.toLowerCase().indexOf(filter.toLowerCase()) >= 0);
}
private _onRenderCell = (item: IWorkItemListItem, index: number) => {
return (
<DraggableWorkItemListItemRenderer {...item} onClick={this.props.launchWorkItemForm} />
);
}
}
const makeMapStateToProps = () => {
return (state: IFeatureTimelineRawState) => {
return {
workItems: unplannedFeaturesSelector()(state),
filter: planFeatureStateSelector()(state).filter
}
}
}
const mapDispatchToProps = (dispatch) => {
return {
launchWorkItemForm: (id: number) => {
if (id) {
dispatch(launchWorkItemForm(id));
}
},
onPlanFeaturesFilterChanged: (filter: string) => {
dispatch(changePlanFeaturesFilter(filter));
}
};
};
export const ConnectedWorkItemsList = connect(
makeMapStateToProps, mapDispatchToProps
)(WorkItemList);
import * as React from 'react';
import { connect } from 'react-redux';
import { IFeatureTimelineRawState } from '../../../FeatureTimeline/redux/store/types';
import { unplannedFeaturesSelector, planFeatureStateSelector } from '../../../FeatureTimeline/redux/selectors';
import { launchWorkItemForm } from "../../redux/actions/launchWorkItemForm";
import { List } from 'office-ui-fabric-react/lib/List';
import DraggableWorkItemListItemRenderer from './WorkItem/DraggableWorkItemListItemRenderer';
import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { changePlanFeaturesFilter } from '../../../FeatureTimeline/redux/store/common/actioncreators';
export interface IWorkItemListItem {
id: number;
title: string;
color: string;
inProgressState: string;
order: number;
}
export interface IWorkItemListProps {
workItems: IWorkItemListItem[];
filter: string;
launchWorkItemForm: (id: number) => void;
onPlanFeaturesFilterChanged: (filter: string) => void;
}
export interface IWorkItemListState {
}
class WorkItemList extends React.Component<IWorkItemListProps, IWorkItemListState> {
public constructor(props: IWorkItemListProps) {
super(props);
this.state = {};
}
public render() {
if (this.props.workItems.length === 0 || !this.props.workItems) {
return (
<MessageBar>No new features to plan.</MessageBar>
);
}
const workItems = this._filteredItems(this.props.workItems);
let message = null;
if (workItems.length === 0) {
message = <MessageBar>Only top 100 Proposed features are available.</MessageBar>;
}
return (
<div className="work-item-list-container">
<TextField
placeholder="Search Features"
onChanged={this._changeFilter}
value={this.props.filter}
/>
<List
items={workItems}
onRenderCell={this._onRenderCell}
/>
{message}
</div>
);
}
private _changeFilter = (text: string) => {
this.props.onPlanFeaturesFilterChanged(text);
}
private _filteredItems = (arg0: IWorkItemListItem[]): IWorkItemListItem[] => {
const {
filter
} = this.props;
if (!filter || !arg0) {
return arg0;
}
return arg0.filter(w => w.title.toLowerCase().indexOf(filter.toLowerCase()) >= 0);
}
private _onRenderCell = (item: IWorkItemListItem, index: number) => {
return (
<DraggableWorkItemListItemRenderer {...item} onClick={this.props.launchWorkItemForm} />
);
}
}
const makeMapStateToProps = () => {
return (state: IFeatureTimelineRawState) => {
return {
workItems: unplannedFeaturesSelector()(state),
filter: planFeatureStateSelector()(state).filter
}
}
}
const mapDispatchToProps = (dispatch) => {
return {
launchWorkItemForm: (id: number) => {
if (id) {
dispatch(launchWorkItemForm(id));
}
},
onPlanFeaturesFilterChanged: (filter: string) => {
dispatch(changePlanFeaturesFilter(filter));
}
};
};
export const ConnectedWorkItemsList = connect(
makeMapStateToProps, mapDispatchToProps
)(WorkItemList);

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

@ -1,17 +1,17 @@
export 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})`;
}
}
export 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,76 @@
import { TeamSettingsIteration } from "TFS/Work/Contracts";
import { WorkItem, WorkItemStateColor } from "TFS/WorkItemTracking/Contracts";
import { IIterationDuration } from "./IIterationDuration";
import { CropWorkItem, IDimension } from "./types";
import { ISettingsState } from "../modules/SettingsState/SettingsStateContracts";
export interface IWorkItemDisplayDetails {
id: number;
title: string;
color: string;
workItemStateColor: WorkItemStateColor;
isRoot: boolean;
workItem: WorkItem;
order: number;
iterationDuration: IIterationDuration;
showInfoIcon: boolean;
isComplete: boolean;
efforts: number;
childrenWithNoEfforts: number;
children: IWorkItemDisplayDetails[];
predecessors: WorkItem[];
successors: WorkItem[];
highlightPredecessorIcon: boolean;
highlighteSuccessorIcon: boolean;
}
export interface IGridIteration {
teamIteration: TeamSettingsIteration;
dimension: IDimension;
}
export interface IProgressIndicator {
total: number;
completed: number;
}
export interface IGridItem {
dimension: IDimension;
}
export interface IGridWorkItem extends IGridItem {
workItem: IWorkItemDisplayDetails;
progressIndicator: IProgressIndicator;
crop: CropWorkItem;
settingsState: ISettingsState;
allowOverrideIteration: boolean;
}
export interface IGridIterationDisplayDetails {
emptyHeaderRow: IDimension[]; //Set of empty elements to place items on top of iteration header
iterationHeader: IGridIteration[];
iterationShadow: IGridIteration[];
}
export interface IGridView extends IGridIterationDisplayDetails {
workItems: IGridWorkItem[];
isSubGrid: boolean;
shadowForWorkItemId: number;
hideParents: boolean;
iterationDisplayOptions: IIterationDisplayOptions;
teamIterations: TeamSettingsIteration[];
backlogIteration: TeamSettingsIteration,
currentIterationIndex: number;
separators: IDimension[];
}
export interface IIterationDisplayOptions {
totalIterations: number
originalCount: number;
count: number;
startIndex: number;
endIndex: number;
teamId: string;
projectId: string;
}

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

@ -0,0 +1,17 @@
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
export interface IIterationDuration {
startIteration: TeamSettingsIteration;
endIteration: TeamSettingsIteration;
kind: IterationDurationKind;
kindMessage: string; // Little more descriptive message to indicate why the specific kin was used
overridedBy?: string; // User name for the case when kind is UserOverridden
childrenAreOutofBounds; // Used to show a warning if there are any children that are outside of the bounds of the work item start/end iteration
}
export enum IterationDurationKind {
BacklogIteration = "BacklogIteration",
Self = "Self",
ChildRollup = "ChildRollup",
UserOverridden = "UserOverridden",
Predecessors = "Predecessors",
}

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

@ -0,0 +1,43 @@
export interface IContributionContext {
level: string;
team: {
id: string;
name: string;
};
workItemTypes: string[];
host: {
background?: boolean;
};
}
export enum UIStatus {
Default,
Loading,
Error,
NoWorkItems,
NoTeamIterations,
OutofScopeTeamIterations
}
export enum CropWorkItem {
None,
Left,
Right,
Both
}
export interface IDimension {
startRow: number;
startCol: number;
endRow: number;
endCol: number;
}
export enum StateCategory {
Proposed,
InProgress,
Resolved,
Completed,
Removed
}

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

@ -0,0 +1,27 @@
import { Action } from "redux";
/**
* A better typing for the Redux Action
*/
// tslint:disable-next-line:interface-name
export interface ActionWithPayload<T extends string, P> extends Action<T> {
/**
* The payload of this action
*/
payload: P;
}
/**
* Create a new action with type and payload
* @param type The action type
* @param payload The payload
*/
export function createAction<T extends string>(type: T): Action<T>;
export function createAction<T extends string, P>(type: T, payload: P, meta?: IDictionaryStringTo<string>): ActionWithPayload<T, P>;
// tslint:disable-next-line:typedef
export function createAction<T extends string, P>(type: T, payload?: P, meta?: IDictionaryStringTo<string>) {
return { type, payload, meta };
}
type ActionsCreatorsMapObject = { [actionCreator: string]: (...args: any[]) => any };
export type ActionsUnion<A extends ActionsCreatorsMapObject> = ReturnType<A[keyof A]>;

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

@ -1,71 +1,73 @@
import { WorkItem } from "TFS/WorkItemTracking/Contracts";
import { WorkItemTrackingHttpClient } from "TFS/WorkItemTracking/RestClient";
import * as VSS_Service from 'VSS/Service';
export class PageWorkItemHelper {
/**
* Pages work items with given id
* Considers the length of the constructed url and does paging based in the lenght of the url + max page size
*/
public static pageWorkItems(ids: number[], projectName?: string, fields?: string[]): IPromise<WorkItem[]> {
const pwh = PageWorkItemHelper;
// Calculate length of the field query parameter
const fieldLength = fields ? `&fields=${fields.join("%2C")}`.length : 0;
const urlPrefixLength = 400; // This is a worst case approximation which include account name and route
const maxUrlLength = 2000;
const allowedLength = maxUrlLength - urlPrefixLength - fieldLength;
const maxPageSize = 200;
const promises: IPromise<WorkItem[]>[] = [];
let pageIds: number[] = [];
let currentLength = 0;
for (const id of ids) {
const idLength = `${id}%2C`.length;
// If we have exceeded url length or page size, page the work items
if ((currentLength + idLength) >= allowedLength || pageIds.length >= maxPageSize) {
promises.push(pwh._pageWorkItems(pageIds, projectName, fields));
pageIds = [id];
currentLength = idLength;
} else {
pageIds.push(id);
currentLength += idLength;
}
}
// Page remaining work items
if (pageIds.length > 0) {
promises.push(pwh._pageWorkItems(pageIds, projectName, fields));
}
return Promise.all(promises)
.then(results => {
const workItems: WorkItem[] = [];
for (const result of results) {
workItems.push(...result);
}
return workItems;
});
}
private static _pageWorkItems(ids: number[], projectName?: string, fieldRefNames?: string[]): IPromise<WorkItem[]> {
const witHttpClient = PageWorkItemHelper._getHttpClient();
return witHttpClient.getWorkItems(ids,
fieldRefNames,
null, // asof
null, // expand
null, // error policy
projectName
);
}
private static _getHttpClient(): WorkItemTrackingHttpClient {
return VSS_Service.getClient(WorkItemTrackingHttpClient);;
}
import { WorkItem } from "TFS/WorkItemTracking/Contracts";
import { WorkItemTrackingHttpClient } from "TFS/WorkItemTracking/RestClient";
import * as VSS_Service from 'VSS/Service';
export class PageWorkItemHelper {
/**
* Pages work items with given id
* Considers the length of the constructed url and does paging based in the lenght of the url + max page size
*/
public static pageWorkItems(ids: number[], projectName?: string, fields?: string[]): IPromise<WorkItem[]> {
const pwh = PageWorkItemHelper;
fields = fields.filter(f => !!f);
ids = ids.filter(i => !!i);
// Calculate length of the field query parameter
const fieldLength = fields ? `&fields=${fields.join("%2C")}`.length : 0;
const urlPrefixLength = 400; // This is a worst case approximation which include account name and route
const maxUrlLength = 2000;
const allowedLength = maxUrlLength - urlPrefixLength - fieldLength;
const maxPageSize = 200;
const promises: IPromise<WorkItem[]>[] = [];
let pageIds: number[] = [];
let currentLength = 0;
for (const id of ids) {
const idLength = `${id}%2C`.length;
// If we have exceeded url length or page size, page the work items
if ((currentLength + idLength) >= allowedLength || pageIds.length >= maxPageSize) {
promises.push(pwh._pageWorkItems(pageIds, projectName, fields));
pageIds = [id];
currentLength = idLength;
} else {
pageIds.push(id);
currentLength += idLength;
}
}
// Page remaining work items
if (pageIds.length > 0) {
promises.push(pwh._pageWorkItems(pageIds, projectName, fields));
}
return Promise.all(promises)
.then(results => {
const workItems: WorkItem[] = [];
for (const result of results) {
workItems.push(...result);
}
return workItems;
});
}
private static _pageWorkItems(ids: number[], projectName?: string, fieldRefNames?: string[]): IPromise<WorkItem[]> {
const witHttpClient = PageWorkItemHelper._getHttpClient();
return witHttpClient.getWorkItems(ids,
fieldRefNames,
null, // asof
null, // expand
null, // error policy
projectName
);
}
private static _getHttpClient(): WorkItemTrackingHttpClient {
return VSS_Service.getClient(WorkItemTrackingHttpClient);;
}
}

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

@ -0,0 +1,26 @@
import { IWorkItemDisplayDetails } from "../Contracts/GridViewContracts";
import { ProgressTrackingCriteria } from "../modules/SettingsState/SettingsStateContracts";
export function getProgress(children: IWorkItemDisplayDetails[], criteria: ProgressTrackingCriteria) {
const completedChildren = children.filter(c => c.isComplete);
switch (criteria) {
case ProgressTrackingCriteria.ChildWorkItems: {
return {
total: children.length,
completed: completedChildren.length
};
}
case ProgressTrackingCriteria.EffortsField: {
return {
total: getEfforts(children),
completed: getEfforts(completedChildren)
};
}
}
return {
total: 0,
completed: 0
};
}
function getEfforts(workItems: IWorkItemDisplayDetails[]): number {
return workItems.reduce((prev, w) => prev + w.efforts, 0);
}

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

@ -0,0 +1,12 @@
import { IIterationDuration, IterationDurationKind } from "../Contracts/IIterationDuration";
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
export function areChildrenOutOfBounds(parentStartIteration: TeamSettingsIteration, parentEndIteration: TeamSettingsIteration, childrenIterationDuration: IIterationDuration, allIterations: TeamSettingsIteration[]): boolean {
if (childrenIterationDuration.kind === IterationDurationKind.BacklogIteration || !parentStartIteration || !parentEndIteration) {
return false;
}
const startIndex = allIterations.findIndex(itr => itr.id == parentStartIteration.id);
const endIndex = allIterations.findIndex(itr => itr.id == parentEndIteration.id);
const childStartIndex = allIterations.findIndex(itr => itr.id == childrenIterationDuration.startIteration.id);
const childEndIndex = allIterations.findIndex(itr => itr.id == childrenIterationDuration.endIteration.id);
return childStartIndex < startIndex || childEndIndex > endIndex;
}

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

@ -0,0 +1,3 @@
export function escapeStr(value: string): string {
return value.replace("'", "''");
}

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

@ -0,0 +1,50 @@
import { TeamSettingsIteration } from "TFS/Work/Contracts";
import { IGridIteration, IGridIterationDisplayDetails, IGridWorkItem } from "../Contracts/GridViewContracts";
import { IDimension } from "../Contracts/types";
export function getIterationDisplayDetails(gridWorkItems: IGridWorkItem[], displayIterations: TeamSettingsIteration[], hideParents: boolean) {
const gridIterationDisplayDetails: IGridIterationDisplayDetails = {
emptyHeaderRow: [],
iterationHeader: [],
iterationShadow: []
};
// 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
};
gridIterationDisplayDetails.emptyHeaderRow.push(emptyRowDimension);
const dimension: IDimension = {
startCol,
startRow: startRow + 1,
endRow,
endCol
};
const gridIteration: IGridIteration = {
teamIteration,
dimension
};
gridIterationDisplayDetails.iterationHeader.push(gridIteration);
const shadowDimension: IDimension = {
startRow: startRow + 2,
startCol,
endCol,
endRow: lastWorkItemRow
};
const shadowIteration: IGridIteration = {
teamIteration,
dimension: shadowDimension
};
gridIterationDisplayDetails.iterationShadow.push(shadowIteration);
startCol++;
});
return gridIterationDisplayDetails;
}

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

@ -1,24 +1,24 @@
import { WorkItemTypeStateInfo } from "TFS/Work/Contracts";
import { StateCategory } from "../store/workitems/types";
export function getWorkItemStateCategory(
workItemType: string,
state: string,
workItemTypeMappedStates: WorkItemTypeStateInfo[]): StateCategory {
const stateInfo: WorkItemTypeStateInfo = workItemTypeMappedStates
.filter(wtms => wtms.workItemTypeName.toLowerCase() === workItemType.toLowerCase())[0];
return StateCategory[stateInfo.states[state]];
}
export function getDefaultInProgressState(
workItemType: string,
workItemTypeMappedStates: WorkItemTypeStateInfo[]): string {
const stateInfo: WorkItemTypeStateInfo = workItemTypeMappedStates
.filter(wtms => wtms.workItemTypeName.toLowerCase() === workItemType.toLowerCase())[0];
return Object.keys(stateInfo.states).filter(s => stateInfo.states[s] === "InProgress")[0];
}
import { WorkItemTypeStateInfo } from "TFS/Work/Contracts";
import { StateCategory } from "../Contracts/types";
export function getWorkItemStateCategory(
workItemType: string,
state: string,
workItemTypeMappedStates: WorkItemTypeStateInfo[]): StateCategory {
const stateInfo: WorkItemTypeStateInfo = workItemTypeMappedStates
.filter(wtms => wtms.workItemTypeName.toLowerCase() === workItemType.toLowerCase())[0];
return StateCategory[stateInfo.states[state]];
}
export function getDefaultInProgressState(
workItemType: string,
workItemTypeMappedStates: WorkItemTypeStateInfo[]): string {
const stateInfo: WorkItemTypeStateInfo = workItemTypeMappedStates
.filter(wtms => wtms.workItemTypeName.toLowerCase() === workItemType.toLowerCase())[0];
return Object.keys(stateInfo.states).filter(s => stateInfo.states[s] === "InProgress")[0];
}

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

@ -1,34 +1,34 @@
import { IDimension } from "../../redux/types";
export function getRowColumnStyle(dimension: IDimension) {
if (!dimension) {
return {};
}
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}`
};
import { IDimension } from "../Contracts/types";
export function getRowColumnStyle(dimension: IDimension) {
if (!dimension) {
return {};
}
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}`
};
}

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

@ -1,81 +1,81 @@
import { TeamSettingsIteration, TimeFrame } 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);
}
export function getCurrentIterationIndex(iterations: TeamSettingsIteration[]): number {
let index = 0;
if (TimeFrame) {
index = iterations.findIndex(i => i.attributes.timeFrame === TimeFrame.Current);
} else {
index = iterations.findIndex(_isCurrentIteration);
}
if (index < 0 && iterations.length > 0) {
index = 0;
}
return index;
}
export function isCurrentIteration(iterations: TeamSettingsIteration[], iteration: TeamSettingsIteration): boolean {
const index = getCurrentIterationIndex(iterations);
const currentIteration = iterations[index];
return currentIteration.id === iteration.id;
}
function _isCurrentIteration(iteration: TeamSettingsIteration): boolean {
if (TimeFrame) {
return iteration.attributes.timeFrame === TimeFrame.Current;
}
const today = new Date(Date.now());
const currentTimeStamp = (new Date(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate())).getTime();
if (!hasDates(iteration)
|| (iteration.attributes.startDate.getTime() <= currentTimeStamp
&& iteration.attributes.finishDate.getTime() >= currentTimeStamp)) {
return true;
}
return false;
}
function hasDates(iteration: TeamSettingsIteration): boolean {
if (!iteration || !iteration.attributes) {
return false;
}
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();
import { TeamSettingsIteration, TimeFrame } 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);
}
export function getCurrentIterationIndex(iterations: TeamSettingsIteration[]): number {
let index = 0;
if (TimeFrame) {
index = iterations.findIndex(i => i.attributes.timeFrame === TimeFrame.Current);
} else {
index = iterations.findIndex(_isCurrentIteration);
}
if (index < 0 && iterations.length > 0) {
index = 0;
}
return index;
}
export function isCurrentIteration(iterations: TeamSettingsIteration[], iteration: TeamSettingsIteration): boolean {
const index = getCurrentIterationIndex(iterations);
const currentIteration = iterations[index];
return currentIteration.id === iteration.id;
}
function _isCurrentIteration(iteration: TeamSettingsIteration): boolean {
if (TimeFrame) {
return iteration.attributes.timeFrame === TimeFrame.Current;
}
const today = new Date(Date.now());
const currentTimeStamp = (new Date(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate())).getTime();
if (!hasDates(iteration)
|| (iteration.attributes.startDate.getTime() <= currentTimeStamp
&& iteration.attributes.finishDate.getTime() >= currentTimeStamp)) {
return true;
}
return false;
}
function hasDates(iteration: TeamSettingsIteration): boolean {
if (!iteration || !iteration.attributes) {
return false;
}
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,15 @@
import { IContributionContext } from "../Contracts/types";
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;
};

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

@ -0,0 +1,69 @@
import { TeamSettingsIteration } from "TFS/Work/Contracts";
import { IIterationDisplayOptions, IWorkItemDisplayDetails } from "../Contracts/GridViewContracts";
import { compareIteration } from "../Helpers/iterationComparer";
export function getDisplayIterations(
backlogIteration: TeamSettingsIteration,
teamIterations: TeamSettingsIteration[],
workItems: IWorkItemDisplayDetails[],
canIncludeBacklogIteration: boolean,
iterationDisplayOptions?: IIterationDisplayOptions): TeamSettingsIteration[] {
// Sort the input iteration
teamIterations = teamIterations.slice().sort(compareIteration);
const validIterationDisplayOptions = iterationDisplayOptions && iterationDisplayOptions.startIndex != null && iterationDisplayOptions.endIndex != null;
if (validIterationDisplayOptions) {
return teamIterations.slice(iterationDisplayOptions.startIndex, iterationDisplayOptions.endIndex + 1);
}
let firstIteration: TeamSettingsIteration = null;
let lastIteration: TeamSettingsIteration = null;
let atleastHaveOneBacklogIteation = false;
// Get all iterations that come in the range of the workItems
const calcFirstLastIteration = (workItem: IWorkItemDisplayDetails) => {
const {
startIteration,
endIteration
} = workItem.iterationDuration;
if (startIteration.id !== backlogIteration.id && endIteration.id !== backlogIteration.id) {
if (firstIteration === null) {
firstIteration = startIteration;
lastIteration = endIteration;
}
else {
if (compareIteration(startIteration, firstIteration) < 0) {
firstIteration = startIteration;
}
if (compareIteration(endIteration, lastIteration) > 0) {
lastIteration = endIteration;
}
}
} else {
atleastHaveOneBacklogIteation = true;
}
workItem.children.forEach(child => calcFirstLastIteration(child));
};
workItems.forEach(calcFirstLastIteration);
// If there are no planned workitems use first and last team iteration
if (!firstIteration || !lastIteration) {
firstIteration = teamIterations[0];
lastIteration = teamIterations[teamIterations.length - 1];
}
const additionalIterations = canIncludeBacklogIteration ? 1 : 2;
// Get two to the left and two to the right iterations from candiateIterations
let startIndex = teamIterations.findIndex(i => i.id === firstIteration.id) - additionalIterations;
let endIndex = teamIterations.findIndex(i => i.id === lastIteration.id) + additionalIterations;
startIndex = startIndex < 0 ? 0 : startIndex;
endIndex = endIndex >= teamIterations.length ? teamIterations.length - 1 : endIndex;
const displayIterations = teamIterations.slice(startIndex, endIndex + 1);
if (canIncludeBacklogIteration) {
displayIterations.push(backlogIteration);
}
canIncludeBacklogIteration = canIncludeBacklogIteration && atleastHaveOneBacklogIteation;
return displayIterations;
}

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

@ -0,0 +1,22 @@
import { Action } from "redux";
import { TeamSettingsIteration } from "TFS/Work/Contracts";
import { ActionCreator } from 'redux';
export const StartUpdateWorkitemIterationActionType = "@@workitems/StartUpdateWorkitemIterationAction";
export interface StartUpdateWorkitemIterationAction extends Action {
type: "@@workitems/StartUpdateWorkitemIterationAction";
payload: {
workItem: number;
teamIteration: TeamSettingsIteration;
override: boolean;
};
}
export const startUpdateWorkItemIteration: ActionCreator<StartUpdateWorkitemIterationAction> = (workItem: number, teamIteration: TeamSettingsIteration, override: boolean) => ({
type: StartUpdateWorkitemIterationActionType,
payload: {
workItem,
teamIteration,
override
}
});

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

@ -0,0 +1,17 @@
import { ActionCreator, Action } from 'redux';
export const LaunchWorkItemFormActionType = "@@workitems/LaunchWorkItemForm";
export interface LaunchWorkItemFormAction extends Action {
type: "@@workitems/LaunchWorkItemForm";
payload: {
workItemId: number;
}
}
export const launchWorkItemForm: ActionCreator<LaunchWorkItemFormAction> = (workItemId: number) => ({
type: LaunchWorkItemFormActionType,
track: true,
payload: {
workItemId
}
});

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

@ -0,0 +1,38 @@
import { ActionsUnion, createAction } from "../../Helpers/ActionHelper";
export const HighlightDependenciesType = "@@PredecessorSuccessor/HighlightDependencies";
export const DismissDependenciesType = "@@PredecessorSuccessor/DismissDependencies";
export interface IHighlightedDependency {
id: number,
highlightSuccesors: boolean
}
export interface IHighlightDependenciesAwareState {
highlightedDependency: IHighlightedDependency
}
export const HighlightDependenciesActionsCreator = {
highlightDependencies: (id: number, highlightSuccesors: boolean) =>
createAction(HighlightDependenciesType, { id, highlightSuccesors }),
dismissDependencies: () =>
createAction(DismissDependenciesType),
}
type HighlightDependenciesActions = ActionsUnion<typeof HighlightDependenciesActionsCreator>;
export function highlightDependencyReducer(state: IHighlightedDependency, action: HighlightDependenciesActions): IHighlightedDependency {
if (!state) {
state = { id: undefined, highlightSuccesors: undefined };
}
switch (action.type) {
case HighlightDependenciesType: {
return action.payload
}
case DismissDependenciesType: {
return { id: undefined, highlightSuccesors: undefined };
}
}
return state;
}
export const highlightDependenciesSelector = (state: IHighlightDependenciesAwareState) => state.highlightedDependency;

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

@ -0,0 +1,95 @@
import { IIterationDisplayOptions } from "../../Contracts/GridViewContracts";
import { Action } from "redux";
import { ActionCreator } from "react-redux";
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,
currentIterationIndex: number,
maxIterations: number
}
}
export interface RestoreDisplayIterationCountAction extends Action {
type: "@@TeamSettingsIteration/RestoreDisplayIterationCountAction";
payload: {
displayOptions: IIterationDisplayOptions
}
}
export interface ShiftDisplayIterationLeftAction extends Action {
type: "@@TeamSettingsIteration/ShiftDisplayIterationLeftAction";
payload: {
count: number,
maxIterations: number
}
}
export interface ShiftDisplayIterationRightAction extends Action {
type: "@@TeamSettingsIteration/ShiftDisplayIterationRightAction";
payload: {
count: number,
maxIterations: number
}
}
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 const displayAllIterations: ActionCreator<DisplayAllIterationsAction> =
() => ({
type: DisplayAllIterationsActionType,
payload: null
});
export const changeDisplayIterationCount: ActionCreator<ChangeDisplayIterationCountAction> =
(count: number, projectId: string, teamId: string, maxIterations: number, currentIterationIndex: number) => ({
type: ChangeDisplayIterationCountActionType,
payload: {
count,
projectId,
teamId,
maxIterations,
currentIterationIndex
}
});
export const restoreDisplayIterationCount: ActionCreator<RestoreDisplayIterationCountAction> =
(displayOptions: IIterationDisplayOptions, maxIterations: number) => ({
type: RestoreDisplayIterationCountActionType,
payload: {
displayOptions,
maxIterations
}
});
export const shiftDisplayIterationLeft: ActionCreator<ShiftDisplayIterationLeftAction> =
(count: number, maxIterations: number) => ({
type: ShiftDisplayIterationLeftActionType,
payload: {
count,
maxIterations
}
});
export const shiftDisplayIterationRight: ActionCreator<ShiftDisplayIterationRightAction> =
(count: number, maxIterations: number) => ({
type: ShiftDisplayIterationRightActionType,
payload: {
count,
maxIterations
}
});
export type IterationDisplayActions = DisplayAllIterationsAction | ChangeDisplayIterationCountAction | RestoreDisplayIterationCountAction | ShiftDisplayIterationLeftAction | ShiftDisplayIterationRightAction;

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

@ -0,0 +1,5 @@
import { IIterationDisplayOptions } from "../../Contracts/GridViewContracts";
export interface IIterationDisplayOptionsAwareState {
iterationDisplayOptions: IIterationDisplayOptions;
}

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

@ -0,0 +1,131 @@
import { Reducer } from 'redux';
import produce from "immer";
import {
IterationDisplayActions, DisplayAllIterationsActionType, ShiftDisplayIterationLeftActionType, ShiftDisplayIterationRightActionType,
ChangeDisplayIterationCountActionType, RestoreDisplayIterationCountActionType, ShiftDisplayIterationRightAction, ShiftDisplayIterationLeftAction,
ChangeDisplayIterationCountAction, RestoreDisplayIterationCountAction
} from './IterationDisplayOptionsActions';
import { IIterationDisplayOptions } from '../../Contracts/GridViewContracts';
// Type-safe initialState!
export const getInitialState = (): IIterationDisplayOptions => {
return null;
};
export const iterationDisplayOptionsReducer: Reducer<IIterationDisplayOptions> =
(state: IIterationDisplayOptions,
action: IterationDisplayActions) => {
if (!state) {
state = getInitialState();
}
switch (action.type) {
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: IIterationDisplayOptions, action: RestoreDisplayIterationCountAction) {
const {
displayOptions
} = action.payload;
try {
let newDisplayOptions = { ...displayOptions };
let { count } = displayOptions;
const maxIterations = displayOptions.totalIterations;
newDisplayOptions.totalIterations = maxIterations;
// Handle incase if the team iterations changed before restore
if (maxIterations === 0 || !count || count > maxIterations || displayOptions.endIndex >= maxIterations) {
console.log("Ignoring restore display options as iterations changed.");
newDisplayOptions = null;
}
return newDisplayOptions;
}
catch (error) {
console.log('Can not restore display options: ', error, action);
}
return null;
}
function handleChangeDisplayIterationCountAction(state: IIterationDisplayOptions, action: ChangeDisplayIterationCountAction) {
let {
count,
teamId,
projectId,
maxIterations,
currentIterationIndex
} = action.payload;
const originalCount = count;
if (count > maxIterations) {
count = maxIterations;
}
const displayOptions = {
count,
originalCount,
teamId,
projectId,
startIndex: 0,
endIndex: 0,
totalIterations: maxIterations
};
let startIndex = currentIterationIndex - Math.floor((count / 2));
if (startIndex < 0) {
startIndex = 0;
}
const endIndex = startIndex + (count - 1);
displayOptions.startIndex = startIndex;
displayOptions.endIndex = endIndex;
return displayOptions;
}
function handleShiftDisplayIterationLeft(state: IIterationDisplayOptions, action: ShiftDisplayIterationLeftAction) {
return produce(state, draft => {
if (draft) {
const displayOptions = draft;
if ((displayOptions.startIndex - action.payload.count) >= 0) {
displayOptions.startIndex -= action.payload.count;
displayOptions.endIndex = displayOptions.startIndex + draft.count - 1;
}
draft = displayOptions;
}
});
}
function handleShiftDisplayIterationRight(state: IIterationDisplayOptions, action: ShiftDisplayIterationRightAction) {
return produce(state, draft => {
if (draft) {
const iterationCount = action.payload.maxIterations;
const displayOptions = draft;
if ((displayOptions.endIndex + action.payload.count) < iterationCount) {
displayOptions.endIndex += action.payload.count;
displayOptions.startIndex = displayOptions.endIndex - draft.count + 1;
}
draft = displayOptions;
}
});
}
function handleDisplayAllIterations(state: IIterationDisplayOptions) {
return null;
}

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

@ -0,0 +1,27 @@
import { call, put } from "redux-saga/effects";
import { IIterationDisplayOptions } from "../../Contracts/GridViewContracts";
import { restoreDisplayIterationCount } from "./IterationDisplayOptionsActions";
export function* fetchIterationDisplayOptions(teamId: string, settingsPrefix: string = "") {
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
const iterationDisplayOptions = yield call([dataService, dataService.getValue],
`${settingsPrefix}${teamId}_iterationDisplayOptions`, { scopeType: 'User' });
if (iterationDisplayOptions && iterationDisplayOptions !== "null") {
console.log(`parsed iteration displayoptions`, JSON.parse(iterationDisplayOptions));
yield put(restoreDisplayIterationCount(JSON.parse(iterationDisplayOptions)));
}
}
export function* saveIterationDisplayOptions(
teamId: string,
iterationDisplayOptions: IIterationDisplayOptions,
settingsPrefix: string = "") {
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
const value = !iterationDisplayOptions ? null : JSON.stringify(iterationDisplayOptions);
yield call([dataService, dataService.setValue],
`${settingsPrefix}${teamId}_iterationDisplayOptions`,
value,
{ scopeType: 'User' });
}

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

@ -0,0 +1,4 @@
import { IIterationDisplayOptionsAwareState } from './IterationDisplayOptionsContracts';
export function getIterationDisplayOptionsState(state: IIterationDisplayOptionsAwareState) {
return state.iterationDisplayOptions;
}

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

@ -0,0 +1,20 @@
export type SavedOverriddenIteration = IDictionaryNumberTo<IOverriddenIterationDuration>;
export interface IOverriddenIterationsAwareState {
savedOverriddenIterations: SavedOverriddenIteration;
}
export interface IOverriddenIterationDuration {
startIterationId: string;
endIterationId: string;
user: string;
}
export interface IWorkItemOverrideIterationAwareState {
workItemOverrideIteration: IWorkItemOverrideIteration;
}
export interface IWorkItemOverrideIteration {
workItemId: number;
iterationDuration: IOverriddenIterationDuration;
changingStart: boolean; // Weather we are changing start iteration or end iteration
}

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

@ -0,0 +1,13 @@
import { call, put } from "redux-saga/effects";
import { OverriddenIterationsActionCreator } from './overrideIterationsActions';
export function* restoreOverriddenIterations() {
const dataService
= yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
const overriddenIterations: string =
yield call([dataService, dataService.getValue],
"overriddenWorkItemIterations");
yield put(OverriddenIterationsActionCreator.restore(overriddenIterations ? JSON.parse(overriddenIterations) : null));
}

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

@ -0,0 +1,11 @@
import { IWorkItemOverrideIterationAwareState } from './overriddenIterationContracts';
import { IEpicRoadmapState } from '../../../../EpicRoadmap/redux/contracts';
export const
OverriddenIterationSelector =
(state: IEpicRoadmapState) =>
state.savedOverriddenIterations;
export function getWorkItemOverrideIteration(state: IWorkItemOverrideIterationAwareState) {
return state.workItemOverrideIteration;
}

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

@ -0,0 +1,26 @@
import { IOverriddenIterationDuration } from "./overriddenIterationContracts";
import { createAction, ActionsUnion } from "../../Helpers/ActionHelper";
export const SetOverrideIterationType = "@@workitems/setoverrideiteration";
export const ClearOverrideIterationType = "@@overrideIteration/cleareoverrideiteration";
export const RestoreOverrideIterationType = "@@overrideIteration/restoreeoverrideiteration";
export const OverriddenIterationsActionCreator = {
set: (workItemId: number, details: IOverriddenIterationDuration) =>
createAction(SetOverrideIterationType, {
workItemId,
details
}),
clear: (workItemId: number) =>
createAction(ClearOverrideIterationType, {
workItemId
}),
restore: (details: IDictionaryNumberTo<IOverriddenIterationDuration>) =>
createAction(RestoreOverrideIterationType, {
details
})
}
export type OverriddenIterationActions = ActionsUnion<typeof OverriddenIterationsActionCreator>;

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

@ -0,0 +1,22 @@
import produce from "immer";
import { RestoreOverrideIterationType, OverriddenIterationActions, SetOverrideIterationType, ClearOverrideIterationType } from './overrideIterationsActions';
import { IOverriddenIterationDuration } from './overriddenIterationContracts';
export function savedOverrideIterationsReducer(
state: IDictionaryNumberTo<IOverriddenIterationDuration> = {},
action: OverriddenIterationActions) {
return produce(state, draft => {
switch (action.type) {
case SetOverrideIterationType:
draft[action.payload.workItemId] = action.payload.details;
break;
case ClearOverrideIterationType: {
delete draft[action.payload.workItemId];
break;
}
case RestoreOverrideIterationType: {
return action.payload.details;
}
}
});
};

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

@ -0,0 +1,12 @@
import { ActionsUnion, createAction } from "../../Helpers/ActionHelper";
export const ProgressAwareErrorType = "@@ProgressAware/Error";
export const ProgressAwareLoadingType = "@@ProgressAware/Loading";
export const ProgressAwareActionCreator = {
setError: (error: Error) =>
createAction(ProgressAwareErrorType, error),
setLoading: (loading: boolean) =>
createAction(ProgressAwareLoadingType, loading)
}
export type ProgressAwareActions = ActionsUnion<typeof ProgressAwareActionCreator>;

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

@ -0,0 +1,8 @@
export interface IProgress {
error: Error;
loading: boolean;
}
export interface IProgressAwareState {
progress: IProgress;
}

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

@ -0,0 +1,24 @@
import { IProgress } from "./ProgressAwareStateContracts";
import { ProgressAwareActions, ProgressAwareErrorType, ProgressAwareLoadingType } from "./ProgressAwareStateActions";
import produce from "immer";
export function progressAwareReducer(state: IProgress, action: ProgressAwareActions): IProgress {
if (!state) {
state = {
error: null,
loading: false
};
}
return produce(state, draft => {
switch (action.type) {
case ProgressAwareErrorType:
draft.error = action.payload;
draft.loading = false;
break;
case ProgressAwareLoadingType:
draft.loading = action.payload
break;
}
});
}

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

@ -0,0 +1,62 @@
import { Action } from "redux";
import { ProgressTrackingCriteria, ISettingsState } from "./SettingsStateContracts";
import { ActionCreator } from "react-redux";
export const ToggleShowWorkitemDetailsType = "@@common/toggleshowworkitemdetails";
export const ChangeProgressTrackingCriteriaType = "@@common/changeprogresstrackingcriteria";
export const ChangeShowClosedSinceDaysType = "@@common/changeshowclosedsincedays";
export const RestoreSettingsType = "@@common/restoresettings";
export const SelectEpicType = "@@common/selectepic";
export interface ToggleShowWorkItemDetailsAction extends Action {
type: "@@common/toggleshowworkitemdetails",
payload: boolean;
}
export interface ChangeProgressTrackingCriteriaAction extends Action {
type: "@@common/changeprogresstrackingcriteria",
payload: ProgressTrackingCriteria;
}
export interface ChangeShowClosedSinceDaysAction extends Action {
type: "@@common/changeshowclosedsincedays",
payload: number;
}
export interface RestoreSettingsAction extends Action {
type: "@@common/restoresettings",
payload: ISettingsState;
}
export interface SelectEpicAction extends Action {
type: "@@common/selectepic",
payload: number;
}
export type SettingsActions = ToggleShowWorkItemDetailsAction | ChangeProgressTrackingCriteriaAction
| RestoreSettingsAction | ChangeShowClosedSinceDaysAction | SelectEpicAction;
export const toggleShowWorkItemDetails: ActionCreator<ToggleShowWorkItemDetailsAction> = (show: boolean) => ({
type: ToggleShowWorkitemDetailsType,
payload: show
});
export const selectEpic: ActionCreator<SelectEpicAction> = (epicId: number) => ({
type: SelectEpicType,
payload: epicId
});
export const changeProgressTrackingCriteria: ActionCreator<ChangeProgressTrackingCriteriaAction> = (criteria: ProgressTrackingCriteria) => ({
type: ChangeProgressTrackingCriteriaType,
payload: criteria
});
export const changeShowClosedSinceDays: ActionCreator<ChangeShowClosedSinceDaysAction> = (days: number) => ({
type: ChangeShowClosedSinceDaysType,
payload: days
});
export const restoreSettingsState: ActionCreator<RestoreSettingsAction> = (state: ISettingsState) => ({
type: RestoreSettingsType,
payload: state
});

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

@ -0,0 +1,15 @@
export enum ProgressTrackingCriteria {
ChildWorkItems,
EffortsField
}
export interface ISettingsState {
showWorkItemDetails: boolean;
progressTrackingCriteria: ProgressTrackingCriteria;
showClosedSinceDays: number;
lastEpicSelected?: number;
}
export interface ISettingsAwareState {
settingsState: ISettingsState;
}

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

@ -1,36 +1,40 @@
import { Reducer } from 'redux';
import { ISettingsState, ProgressTrackingCriteria } from '../types';
import { SettingsActions, ToggleShowWorkitemDetailsType, ChangeProgressTrackingCriteriaType, RestoreSettingsType, ChangeShowClosedSinceDaysType } from './actions';
import produce from "immer";
export const getDefaultSettingsState = (): ISettingsState => {
return {
showWorkItemDetails: false,
progressTrackingCriteria: 0,
showClosedSinceDays: 0
};
}
const reducer: Reducer<ISettingsState> = (state: ISettingsState = getDefaultSettingsState(), action: SettingsActions) => {
const {
type,
payload
} = action;
if(type === RestoreSettingsType) {
return payload as ISettingsState;
}
return produce(state, draft => {
switch (type) {
case ToggleShowWorkitemDetailsType:
draft.showWorkItemDetails = payload as boolean;
break;
case ChangeProgressTrackingCriteriaType:
draft.progressTrackingCriteria = payload as ProgressTrackingCriteria;
break;
case ChangeShowClosedSinceDaysType:
draft.showClosedSinceDays = payload as number;
break;
}
});
};
export default reducer;
import produce from "immer";
import { RestoreSettingsType, ToggleShowWorkitemDetailsType, ChangeProgressTrackingCriteriaType, ChangeShowClosedSinceDaysType, SettingsActions, SelectEpicType } from "./SettingsStateActions";
import { ISettingsState, ProgressTrackingCriteria } from "./SettingsStateContracts";
export const getDefaultSettingsState = (): ISettingsState => {
return {
showWorkItemDetails: false,
progressTrackingCriteria: 0,
showClosedSinceDays: 0,
lastEpicSelected: undefined
};
}
export function settingsStateReducer(state: ISettingsState = getDefaultSettingsState(), action: SettingsActions): ISettingsState {
const {
type,
payload
} = action;
if (type === RestoreSettingsType) {
return payload as ISettingsState;
}
return produce(state, draft => {
switch (type) {
case ToggleShowWorkitemDetailsType:
draft.showWorkItemDetails = payload as boolean;
break;
case ChangeProgressTrackingCriteriaType:
draft.progressTrackingCriteria = payload as ProgressTrackingCriteria;
break;
case ChangeShowClosedSinceDaysType:
draft.showClosedSinceDays = payload as number;
break;
case ToggleShowWorkitemDetailsType:
draft.showWorkItemDetails = payload as boolean;
break;
case SelectEpicType:
draft.lastEpicSelected = payload as number;
break;
}
});
}

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

@ -0,0 +1,28 @@
import { call, put, select } from 'redux-saga/effects';
import { getTeamId } from '../../Selectors/CommonSelectors';
import { restoreSettingsState } from './SettingsStateActions';
import { getSettingsState } from './SettingsStateSelector';
import { ISettingsState } from './SettingsStateContracts';
export function* saveSettings(settingsPrefix: string = "") {
let teamId = yield select(getTeamId);
let value = yield select(getSettingsState);
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
value = value ? JSON.stringify(value) : null;
yield call([dataService, dataService.setValue], `${settingsPrefix}${teamId}_settings`, value, { scopeType: 'User' });
}
export function* restoreSettings(settingsPrefix: string = "") {
let teamId = yield select(getTeamId);
const dataService = yield call(VSS.getService, VSS.ServiceIds.ExtensionData);
const stateString = yield call([dataService, dataService.getValue], `${settingsPrefix}${teamId}_settings`, { scopeType: 'User' });
if (stateString) {
const state = JSON.parse(stateString) as ISettingsState;
yield put(restoreSettingsState(state));
return state;
}
return null;
}

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

@ -0,0 +1,6 @@
import { ISettingsAwareState } from "./SettingsStateContracts";
import { getDefaultSettingsState } from "./SettingsStateReducer";
export function getSettingsState(state: ISettingsAwareState) {
return state.settingsState || getDefaultSettingsState();
}

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

@ -0,0 +1,36 @@
import { Action } from "redux";
import { ActionCreator } from "react-redux";
export const ShowDetailsType = "@@common/showdetails";
export const CloseDetailsType = "@@common/closedetails";
export interface ShowDetailsAction extends Action {
type: "@@common/showdetails";
payload: {
id: number;
}
}
export interface CloseDetailsAction extends Action {
type: "@@common/closedetails";
payload: {
id: number;
}
}
export const showDetails: ActionCreator<ShowDetailsAction> =
(id: number) => ({
type: ShowDetailsType,
payload: { id }
});
export const closeDetails: ActionCreator<CloseDetailsAction> =
(id: number) => ({
type: CloseDetailsType,
payload: { id }
});
export type ShowHideDetailsActions = ShowDetailsAction | CloseDetailsAction;

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

@ -0,0 +1,5 @@
export interface IShowWorkItemInfoAwareState {
// list of work item ids for which the details window is shown
workItemsToShowInfoFor: number[];
}

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

@ -1,18 +1,15 @@
import { Reducer } from 'redux';
import { CommonActions, ShowDetailsType, CloseDetailsType } from './actions';
import produce from "immer";
const reducer: Reducer<number[]> = (state: number[] = [], action: CommonActions) => {
return produce(state, draft => {
switch (action.type) {
case ShowDetailsType:
draft.push(action.payload.id);;
break;
case CloseDetailsType:
return draft.filter(id => id !== action.payload.id);
break;
}
});
};
export default reducer;
import produce from "immer";
import { ShowDetailsType, CloseDetailsType, ShowHideDetailsActions } from "./ShowHideDetailsActions";
export function showHideDetailsReducer(state: number[] = [], action: ShowHideDetailsActions): number[] {
return produce(state, draft => {
switch (action.type) {
case ShowDetailsType:
draft.push(action.payload.id);;
break;
case CloseDetailsType:
return draft.filter(id => id !== action.payload.id);
break;
}
});
};

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

@ -1,38 +1,40 @@
import { ActionCreator } from 'redux';
import { OverrideIterationStartAction, OverrideIterationStartType, OverrideIterationEndAction, OverrideIterationEndType, OverrideIterationHoverOverIterationAction, OverrideIterationHoverOverIterationType, OverrideIterationCleanupAction, OverrideIterationCleanupType, SaveOverrideIterationAction, SaveOverrideIterationActionType } from './actions';
import { IWorkItemOverrideIteration } from '../types';
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
});
import { ActionCreator } from 'redux';
import { OverrideIterationStartAction, OverrideIterationStartType, OverrideIterationEndAction, OverrideIterationEndType,
OverrideIterationHoverOverIterationAction, OverrideIterationHoverOverIterationType, OverrideIterationCleanupAction,
OverrideIterationCleanupType, SaveOverrideIterationAction, SaveOverrideIterationActionType } from './overrideIterationProgressActions';
import { IWorkItemOverrideIteration } from '../OverrideIterations/overriddenIterationContracts';
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
});

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

@ -1,36 +1,36 @@
import { Action } from "redux";
import { IWorkItemOverrideIteration } from "../types";
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
}
import { Action } from "redux";
import { IWorkItemOverrideIteration } from "../OverrideIterations/overriddenIterationContracts";
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;

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

@ -1,29 +1,27 @@
import { Reducer } from 'redux';
import { OverrideIterationActions, OverrideIterationHoverOverIterationType, OverrideIterationStartType, OverrideIterationCleanupType } from './actions';
import { IWorkItemOverrideIteration } from '../types';
import produce from "immer";
const reducer: Reducer<IWorkItemOverrideIteration> = (state: IWorkItemOverrideIteration = null, action: OverrideIterationActions) => {
return produce(state, draft => {
switch (action.type) {
case OverrideIterationStartType:
return { ...action.payload };
case OverrideIterationCleanupType:
return null;
case OverrideIterationHoverOverIterationType: {
if (!state) {
return state;
}
draft.iterationDuration = { ...state.iterationDuration }
if (state.changingStart) {
draft.iterationDuration.startIterationId = action.payload;
} else {
draft.iterationDuration.endIterationId = action.payload;
}
}
}
});
};
export default reducer;
import { Reducer } from 'redux';
import { OverrideIterationActions, OverrideIterationHoverOverIterationType, OverrideIterationStartType, OverrideIterationCleanupType } from './overrideIterationProgressActions';
import produce from "immer";
import { IWorkItemOverrideIteration } from '../OverrideIterations/overriddenIterationContracts';
export const overrideIterationProgressReducer: Reducer<IWorkItemOverrideIteration> = (state: IWorkItemOverrideIteration = null, action: OverrideIterationActions) => {
return produce(state, draft => {
switch (action.type) {
case OverrideIterationStartType:
return { ...action.payload };
case OverrideIterationCleanupType:
return null;
case OverrideIterationHoverOverIterationType: {
if (!state) {
return state;
}
draft.iterationDuration = { ...state.iterationDuration }
if (state.changingStart) {
draft.iterationDuration.startIterationId = action.payload;
} else {
draft.iterationDuration.endIterationId = action.payload;
}
}
}
});
};

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

@ -0,0 +1,10 @@
import { call, select } from 'redux-saga/effects';
import { getTeamId } from '../Selectors/CommonSelectors';
import { saveIterationDisplayOptions } from '../modules/IterationDisplayOptions/iterationDisplayOptionsSaga';
import { getIterationDisplayOptionsState } from '../modules/IterationDisplayOptions/iterationDisplayOptionsSelector';
export function* saveDisplayOptions(settingsPrefix: string = "") {
const displayOptions = yield select(getIterationDisplayOptionsState);
const teamId = yield call(getTeamId);
yield call(saveIterationDisplayOptions, teamId, displayOptions, settingsPrefix);
}

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

@ -1,46 +1,44 @@
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/types';
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 || payload.teamIteration.name
}];
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));
}
}
import { call, put } from "redux-saga/effects";
import { WorkItemTrackingHttpClient3_2 } from 'TFS/WorkItemTracking/RestClient';
import * as VSS_Service from 'VSS/Service';
import { JsonPatchDocument } from 'VSS/WebApi/Contracts';
import { StartUpdateWorkitemIterationAction } from "../actions/StartUpdateWorkitemIterationAction";
import { saveOverrideIteration } from '../modules/overrideIterationProgress/overrideIterationProgressActionCreators';
import { IWorkItemOverrideIteration } from '../modules/OverrideIterations/overriddenIterationContracts';
import { OverriddenIterationsActionCreator } from '../modules/OverrideIterations/overrideIterationsActions';
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 || payload.teamIteration.name
}];
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(OverriddenIterationsActionCreator.clear(payload.workItem));
}
// Update work item Iteration path
yield call(witHttpClient.updateWorkItem.bind(witHttpClient), doc, action.payload.workItem);
}
catch (error) {
}
}

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

@ -1,53 +1,52 @@
import { ClearOverrideIterationAction } from "../store/workitems/actions";
import { put, call, select } from "redux-saga/effects";
import { workItemOverrideIterationSelector } from "../selectors";
import { IWorkItemOverrideIteration } from "../store/types";
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));
import { put, call, select } from "redux-saga/effects";
import { OverriddenIterationsActionCreator } from "../modules/OverrideIterations/overrideIterationsActions";
import { AnyAction } from 'redux';
import { IWorkItemOverrideIteration } from "../modules/OverrideIterations/overriddenIterationContracts";
import { OverrideIterationEndAction, SaveOverrideIterationAction } from "../modules/overrideIterationProgress/overrideIterationProgressActions";
import { cleanupOverrideIteration, saveOverrideIteration } from "../modules/overrideIterationProgress/overrideIterationProgressActionCreators";
import { getWorkItemOverrideIteration } from "../modules/OverrideIterations/overriddenIterationsSelector";
export function* launchOverrideWorkItemIteration(action: OverrideIterationEndAction) {
const overrideIterationState: IWorkItemOverrideIteration = yield select(getWorkItemOverrideIteration);
if (!overrideIterationState) {
return;
}
yield put(cleanupOverrideIteration());
yield put(saveOverrideIteration(overrideIterationState));
}
export function* launchSaveOverrideIteration(action: SaveOverrideIterationAction) {
const overrideIterationState = action.payload;
yield put(OverriddenIterationsActionCreator.set(overrideIterationState.workItemId, overrideIterationState.iterationDuration));
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: AnyAction) {
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.workItemId];
yield call(dataService.setValue.bind(dataService), "overriddenWorkItemIterations", JSON.stringify(currentValues));
}

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

@ -0,0 +1,80 @@
<!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>
VSS.init({
usePlatformStyles: true,
explicitNotifyLoaded: true,
usePlatformScripts: true,
extensionReusedCallback: registerContribution,
moduleLoaderConfig: {
paths: {
"react": "dist/react",
"react-dom": "dist/react-dom",
"EpicRoadmap": "dist/EpicRoadmap"
}
}
});
// We need to register the new contribution if this extension host is reused
function registerContribution(contribution) {
if (contribution.type === "ms.vss-web.tab") {
let title = "Epic Roadmap (Beta)";
if (contribution.id === "ms-devlabs.workitem-feature-timeline-extension-dev.workitem-epic-roadmap") {
title = title + " Dev";
} else if (contribution.id === "ms-devlabs.workitem-feature-timeline-extension-beta.workitem-epic-roadmap") {
title = title + " Beta";
}
// 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: title,
// We set the "dynamic" contribution property to true in the manifest so that it will get the tab name from this function.
name: title,
title: title,
updateContext: updateConfiguration,
isInvisible: function (state) {
return false;
}
});
}
}
let previousContext = null
function updateConfiguration(tabContext) {
}
VSS.ready(function () {
registerContribution(VSS.getContribution());
if (isBackground()) {
VSS.notifyLoadSucceeded();
} else {
// Load main entry point for extension
VSS.require(["EpicRoadmap"], function (er) {
EpicRoadmap = er;
EpicRoadmap.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,23 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { EpicRoadmapView } from "./react/Components/EpicRoadmapView";
import { iePollyfill } from "../polyfill";
export function initialize(): void {
if (!isBackground()) {
iePollyfill();
ReactDOM.render(
<EpicRoadmapView />, 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,136 @@
.feature-timeline-main-container {
display: flex;
flex-direction: column;
height: 100%;
}
.root-container {
height: 100%;
overflow: auto;
}
.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;
}
.non-button {
@include button-dimension();
background: transparent;
border: none;
}
.header-commands {
display: flex;
justify-content: flex-end;
position: absolute;
right: 20px;
background: white;
left: 0;
align-items: center;
}
.plan-feature-checkbox {
margin-left: 15px;
}
.iteration-options {
display: flex;
align-items: center;
justify-content: center;
}
.iteration-options-label {
margin: 5px;
font-size: 16px;
}
.rc-input-number-input{
width: 40px;
text-align: right;
font-size: 16px;
}
.header-gap {
height: 32px;
}
.command-right-section,
.last-header-column-command {
display: flex;
flex-direction: column;
.non-button,
.button {
margin-left: auto;
margin-right: 0;
}
}
.first-header-column-command {
display: flex;
}
.single-column-commands {
display: flex;
justify-content: space-between;
}
#root {
height: 100%;
}
.timeline-splitter {
position: static;
height: calc(100% - 32px);
}
.show-work-item-details-checkbox{
margin-left: 5px;
}
.progress-options,
.closed-since-options {
display: flex;
}
.progress-options-label,
.show-closed-since-label {
margin: 5px;
margin-left: 15px;
font-size: 16px;
}
.progress-options-dropdown,
.show-closed-since-dropdown {
width: 170px;
}
.columnheading {
position: sticky;
top: 34px;
}

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

@ -0,0 +1,450 @@
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { ComboBox } from 'office-ui-fabric-react/lib/ComboBox';
import InputNum from "rc-input-number";
import * as React from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { connect } from 'react-redux';
import { TeamSettingsIteration } from 'TFS/Work/Contracts';
import { IterationDropTarget } from '../../../Common/react/Components/DroppableIterationShadow';
import { IterationRenderer } from '../../../Common/react/Components/IterationRenderer';
import { TeamFieldCard } from '../../../Common/react/Components/TeamField/TeamFieldCard';
import { TeamFieldHeader } from '../../../Common/react/Components/TeamFieldHeader/TeamFieldHeader';
import { ChildRowsSeparator } from '../../../Common/react/Components/WorkItem/ChildRowsSeparatorGap';
import { DraggableWorkItemRenderer } from '../../../Common/react/Components/WorkItem/DraggableWorkItemRenderer';
import { WorkItemShadow } from '../../../Common/react/Components/WorkItem/WorkItemShadow';
import { launchWorkItemForm } from '../../../Common/redux/actions/launchWorkItemForm';
import { startUpdateWorkItemIteration } from '../../../Common/redux/actions/StartUpdateWorkitemIterationAction';
import { getRowColumnStyle, getTemplateColumns } from '../../../Common/redux/Helpers/gridhelper';
import { changeDisplayIterationCount, displayAllIterations, shiftDisplayIterationLeft, shiftDisplayIterationRight } from '../../../Common/redux/modules/IterationDisplayOptions/IterationDisplayOptionsActions';
import { endOverrideIteration, overrideHoverOverIteration, startOverrideIteration } from '../../../Common/redux/modules/overrideIterationProgress/overrideIterationProgressActionCreators';
import { IWorkItemOverrideIteration } from '../../../Common/redux/modules/OverrideIterations/overriddenIterationContracts';
import { OverriddenIterationsActionCreator } from '../../../Common/redux/modules/OverrideIterations/overrideIterationsActions';
import { changeProgressTrackingCriteria, toggleShowWorkItemDetails } from '../../../Common/redux/modules/SettingsState/SettingsStateActions';
import { ProgressTrackingCriteria } from '../../../Common/redux/modules/SettingsState/SettingsStateContracts';
import { closeDetails, showDetails } from '../../../Common/redux/modules/ShowHideDetails/ShowHideDetailsActions';
import { getProjectId, getTeamId } from '../../../Common/redux/Selectors/CommonSelectors';
import { IEpicRoadmapState } from '../../redux/contracts';
import { EpicRoadmapGridViewSelector, IEpicRoadmapGridView } from '../../redux/selectors/EpicRoadmapGridViewSelector';
import './EpicRoadmapGrid.scss';
import { RoadmapTimelineDialog } from './RoadmapTimelineDialog/RoadmapTimelineDialog';
import { HighlightDependenciesActionsCreator } from '../../../Common/redux/modules/HighlightDependencies/HighlightDependenciesModule';
import { IWorkItemRendererProps } from '../../../Common/react/Components/WorkItem/WorkItemRenderer';
export interface IEpicRoadmapGridContentProps {
projectId: string;
teamId: string;
gridView: IEpicRoadmapGridView;
rawState: IEpicRoadmapState,
isSubGrid: boolean,
teamFieldName: string,
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;
showNIterations: (projectId: string, teamId: string, count: Number, maxIterations: number, currentIterationIndex: number) => void;
shiftDisplayIterationLeft: (maxIterations: number) => void;
shiftDisplayIterationRight: (maxIterations: number) => void;
showAllIterations: () => void;
markInProgress: (id: number, teamIteration: TeamSettingsIteration) => void;
toggleShowWorkItemDetails: (show: boolean) => void;
changeProgressTrackingCriteria: (criteria: ProgressTrackingCriteria) => void;
onHighlightDependencies: (id: number, hightlightSuccessor: boolean) => void;
onDismissDependencies: () => void;
}
export class EpicRoadmapGridContent extends React.Component<IEpicRoadmapGridContentProps, {}> {
public render(): JSX.Element {
const {
rawState,
gridView: {
emptyHeaderRow,
iterationHeader,
iterationShadow,
workItems,
separators,
shadowForWorkItemId,
iterationDisplayOptions,
teamIterations,
teamFieldDisplayItems,
teamFieldHeaderItem
},
isSubGrid
} = this.props;
const columnHeading = iterationHeader.map((iteration, index) => {
const style = getRowColumnStyle(iteration.dimension);
return (
<div className="columnheading" style={style}>
<IterationRenderer teamIterations={teamIterations} iteration={iteration.teamIteration} />
</div>
);
});
const shadows = iterationShadow.map((shadow, index) => {
return (
<IterationDropTarget
{...shadow}
isOverrideIterationInProgress={!!rawState.workItemOverrideIteration}
onOverrideIterationOver={this.props.dragHoverOverIteration.bind(this)}
changeIteration={this.props.changeIteration.bind(this)}
markInProgress={this.props.markInProgress.bind(this)}
>
&nbsp;
</IterationDropTarget>
);
});
let workItemShadowCell = null;
if (shadowForWorkItemId > 0) {
const workItem = workItems.filter(w => w.workItem.id === shadowForWorkItemId)[0];
workItemShadowCell = (
<WorkItemShadow dimension={workItem.dimension} twoRows={workItem.settingsState.showWorkItemDetails} />
);
}
const teamFieldCards = teamFieldDisplayItems.map(tfdi => <TeamFieldCard dimension={tfdi.dimension} teamField={tfdi.teamField} />);
const workItemCells = workItems.filter(w => w.workItem.id).map(w => {
const props: IWorkItemRendererProps = {
id: w.workItem.id,
title: w.workItem.title,
color: w.workItem.color,
isRoot: w.workItem.isRoot,
iterationDuration: w.workItem.iterationDuration,
dimension: w.dimension,
onClick: this.props.launchWorkItemForm,
showInfoIcon: w.workItem.showInfoIcon,
showDetails: this.props.showDetails,
overrideIterationStart: this.props.overrideIterationStart,
overrideIterationEnd: this.props.overrideIterationEnd,
allowOverrideIteration: w.allowOverrideIteration,
isSubGrid: isSubGrid,
progressIndicator: w.progressIndicator,
crop: w.crop,
workItemStateColor: w.workItem.workItemStateColor,
settingsState: w.settingsState,
efforts: w.workItem.efforts,
childrernWithNoEfforts: w.workItem.childrenWithNoEfforts,
isComplete: w.workItem.isComplete,
successors: w.workItem.successors,
predecessors: w.workItem.predecessors,
highlightPredecessorIcon: w.workItem.highlightPredecessorIcon,
highlighteSuccessorIcon: w.workItem.highlighteSuccessorIcon,
onHighlightDependencies: this.props.onHighlightDependencies,
onDismissDependencies: this.props.onDismissDependencies,
teamFieldName: this.props.teamFieldName
}
return (
<DraggableWorkItemRenderer
{...props}
/>
);
});
const childRowsSeparator = separators.map(d => {
return (
<ChildRowsSeparator {...d} />
);
});
const extraColumns = this.props.gridView.hideParents ? [] : ['200px'];
let min = '200px';
if (isSubGrid) {
min = '150px';
}
const gridStyle = getTemplateColumns(extraColumns, shadows.length, `minmax(${min}, 300px)`);
let childDialog = null;
if (!this.props.isSubGrid && this.props.rawState.workItemsToShowInfoFor.length > 0) {
childDialog = (
<RoadmapTimelineDialog
{...this.props}
isSubGrid={true}
/>
);
}
let leftButton = <span className="non-button"></span>;
if (iterationDisplayOptions && iterationDisplayOptions.startIndex > 0) {
leftButton = (
<IconButton
className="button"
onClick={() => this.props.shiftDisplayIterationLeft(teamIterations.length)}
iconProps={
{
iconName: "ChevronLeftSmall"
}
}
>
</IconButton>
);
}
let rightButton = <span className="non-button"></span>;
if (iterationDisplayOptions && iterationDisplayOptions.endIndex < (iterationDisplayOptions.totalIterations - 1)) {
rightButton = (
<IconButton
className="button"
onClick={() => this.props.shiftDisplayIterationRight(teamIterations.length)}
iconProps={
{
iconName: "ChevronRightSmall"
}
}
>
</IconButton>
);
}
let displayOptions = null;
let commandHeading = [];
if (!isSubGrid && (iterationDisplayOptions || columnHeading.length > 3)) {
let displayIterationCount = 0;
if (iterationDisplayOptions) {
displayIterationCount = iterationDisplayOptions.count;
} else {
displayIterationCount = teamIterations.length;
}
displayOptions = (
<div className="iteration-options">
<div className="iteration-options-label">View Sprints: </div>
<InputNum
value={displayIterationCount}
min={1}
max={teamIterations.length}
step={1}
onChange={this._onViewChanged}
>
</InputNum>
</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);
}
}
let progressTrackingCriteriaElement = null;
const {
showWorkItemDetails,
progressTrackingCriteria
} = this.props.rawState.settingsState;
if (!isSubGrid && showWorkItemDetails) {
const selectedKey = progressTrackingCriteria === ProgressTrackingCriteria.ChildWorkItems ? "child" : "efforts";
progressTrackingCriteriaElement = (
<div className="progress-options">
<div className="progress-options-label">Track Progress Using: </div>
<ComboBox
className="progress-options-dropdown"
selectedKey={selectedKey}
allowFreeform={false}
autoComplete='off'
options={
[
{ key: 'child', text: 'Completed Stories' },
{ key: 'efforts', text: 'Completed Efforts' }
]
}
onChanged={this._onProgressTrackingCriteriaChanged}
>
</ComboBox>
</div>
);
}
const commands = !isSubGrid && (
<div className="header-commands">
{displayOptions}
<Checkbox
className="show-work-item-details-checkbox"
label={"Show Details"}
onChange={this._onShowWorkItemDetailsChanged}
checked={this.props.rawState.settingsState.showWorkItemDetails} />
{progressTrackingCriteriaElement}
</div>
);
const teamFieldHeader = <TeamFieldHeader dimension={teamFieldHeaderItem} />
const grid = (
<div className="feature-timeline-main-container">
<div className="container" style={gridStyle}>
{commandHeading}
{teamFieldHeader}
{columnHeading}
{shadows}
{workItemShadowCell}
{workItemCells}
{childRowsSeparator}
{teamFieldCards}
{childDialog}
</div>
</div>
);
return (
<div className="root-container" >
{commands}
{<div className="header-gap" ></div>}
{grid}
</div >
);
}
private _onShowWorkItemDetailsChanged = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
this.props.toggleShowWorkItemDetails(checked);
}
private _onViewChanged = (text: string) => {
const {
projectId,
teamId,
gridView: {
teamIterations,
currentIterationIndex
}
} = this.props;
const number = +text;
if (number === 0) {
this.props.showAllIterations();
} else {
this.props.showNIterations(projectId, teamId, number, teamIterations.length, currentIterationIndex);
}
return text;
}
private _onProgressTrackingCriteriaChanged = (item: { key: string, text: string }) => {
const {
changeProgressTrackingCriteria
} = this.props;
switch (item.key) {
case "child":
changeProgressTrackingCriteria(ProgressTrackingCriteria.ChildWorkItems);
break;
case "efforts":
changeProgressTrackingCriteria(ProgressTrackingCriteria.EffortsField);
break;
}
}
}
const makeMapStateToProps = () => {
return (state: IEpicRoadmapState) => {
return {
projectId: getProjectId(),
teamId: getTeamId(),
gridView: EpicRoadmapGridViewSelector(/* isSubGrid */false, /* rootWorkItemId */ state.settingsState.lastEpicSelected)(state), //TODO: This need to come from another selector which is populated by the dropdown
rawState: state,
isSubGrid: false,
teamFieldName: state.backlogConfigurations[getProjectId()].backlogFields.typeFields["Team"]
}
}
}
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(OverriddenIterationsActionCreator.clear(id));
},
changeIteration: (id: number, teamIteration: TeamSettingsIteration, override: boolean) => {
dispatch(startUpdateWorkItemIteration([id], teamIteration, override));
},
markInProgress: (id: number, teamIteration: TeamSettingsIteration, state: string) => {
//dispatch(startMarkInProgress(id, teamIteration, state));
},
showNIterations: (projectId: string, teamId: string, count: Number, maxIterations: number, currentIterationIndex: number) => {
dispatch(changeDisplayIterationCount(count, projectId, teamId, maxIterations, currentIterationIndex));
},
showAllIterations: () => {
dispatch(displayAllIterations());
},
shiftDisplayIterationLeft: (maxIterations: number) => {
dispatch(shiftDisplayIterationLeft(1, maxIterations));
},
shiftDisplayIterationRight: (maxIterations: number) => {
dispatch(shiftDisplayIterationRight(1, maxIterations));
},
toggleShowWorkItemDetails: (show: boolean) => {
dispatch(toggleShowWorkItemDetails(show));
},
changeProgressTrackingCriteria: (criteria: ProgressTrackingCriteria) => {
dispatch(changeProgressTrackingCriteria(criteria));
},
onHighlightDependencies: (id: number, highlightSuccessor: boolean) => {
dispatch(HighlightDependenciesActionsCreator.highlightDependencies(id, highlightSuccessor));
},
onDismissDependencies: () => {
dispatch(HighlightDependenciesActionsCreator.dismissDependencies());
}
};
};
export const EpicRoadmapGrid = DragDropContext(HTML5Backend)(connect(makeMapStateToProps, mapDispatchToProps)(EpicRoadmapGridContent));

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

@ -0,0 +1,18 @@
.epic-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.missing-iteration-name {
font-size: 15px;
}
.simple-work-item-list {
margin-top: 10px;
}
.loading-indicator {
margin-top: 10px;
}

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

@ -0,0 +1,190 @@
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
//import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import * as React from 'react';
import { connect, Provider } from 'react-redux';
import { UIStatus } from '../../../Common/redux/Contracts/types';
import { getProjectId, getTeamId } from '../../../Common/redux/Selectors/CommonSelectors';
import { IEpicRoadmapState } from '../../redux/contracts';
import configureEpicRoadmapStore from '../../redux/EpicRoadmapStore';
import { uiStateSelector, outOfScopeWorkItems } from '../../redux/selectors/uiStateSelector';
import { EpicRoadmapGrid } from './EpicRoadmapGrid';
import { EpicSelector } from './EpicSelector';
import './EpicRoadmapView.scss';
import { WorkItem } from 'TFS/WorkItemTracking/Contracts';
import { launchWorkItemForm } from '../../../Common/redux/actions/launchWorkItemForm';
import { SimpleWorkItem } from '../../../Common/react/Components/WorkItem/SimpleWorkItem';
import { Callout } from 'office-ui-fabric-react/lib/Callout';
initializeIcons(/* optional base url */);
export interface IEpicRoadmapViewProps {
projectId: string;
teamId: string;
uiState: UIStatus;
outOfScopeWorkItems: WorkItem[];
launchWorkItemForm: (id: number) => void;
}
export interface IEpicRoadmapViewContentState {
showCallout: boolean
}
class EpicRoadmapViewContent extends React.Component<IEpicRoadmapViewProps, IEpicRoadmapViewContentState> {
constructor(props, context) {
super(props, context);
this.state = {
showCallout: false
}
}
private _calloutContainer: HTMLDivElement;
public render(): JSX.Element {
const {
uiState,
} = this.props;
if (uiState === UIStatus.Loading) {
return (
<Spinner size={SpinnerSize.large} className="loading-indicator" label="Loading..." />
);
}
let contents = null;
if (uiState === UIStatus.NoTeamIterations) {
contents = (
<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) {
contents = (
<MessageBar
messageBarType={MessageBarType.info}
isMultiline={false}
>
{"Select an Epic."}
</MessageBar>
);
}
let additionalMessage = null;
if (uiState === UIStatus.OutofScopeTeamIterations) {
const style = {cursor: "pointer"};
additionalMessage = (
<MessageBar
messageBarType={MessageBarType.severeWarning}
isMultiline={true}
onClick={this._toggleCallout}
>
<div style={style} ref={ref => this._calloutContainer = ref} onClick={this._toggleCallout}>{"Some Work Items are excluded as they are in iterations that the current team does not subscribe to. Click here to see the details"}</div>
</MessageBar>
);
}
if (uiState === UIStatus.Default || uiState === UIStatus.OutofScopeTeamIterations) {
contents = <EpicRoadmapGrid />;
}
let callout = null;
if (this.state.showCallout) {
callout = this._renderCallout();
}
return (
<div className="epic-container">
<EpicSelector />
{additionalMessage}
{callout}
{contents}
</div>
);
}
private _toggleCallout = () => {
this.setState({
showCallout: !this.state.showCallout
})
}
private _renderCallout = () => {
if (!this._calloutContainer) {
return;
}
const {
outOfScopeWorkItems
} = this.props;
const uniqueIterations = new Set();
outOfScopeWorkItems.forEach(w => uniqueIterations.add(w.fields["System.IterationPath"]));
const iterations = Array.from(uniqueIterations).sort();
return (
<Callout
className="work-item-links-list-callout"
target={this._calloutContainer}
onDismiss={this._toggleCallout}
isBeakVisible={true}
>
<div>{"Please add following iterations in team settings to include these workitems."}</div>
{
iterations.map(i => <div className="missing-iteration-name">{i}</div>)
}
<div className="simple-work-item-list">
{
outOfScopeWorkItems.map(w =>
<SimpleWorkItem
workItem={w}
onShowWorkItem={this.props.launchWorkItemForm}
/>
)
}
</div>
</Callout>
);
}
}
const makeMapStateToProps = () => {
return (state: IEpicRoadmapState) => {
return {
projectId: getProjectId(),
teamId: getTeamId(),
uiState: uiStateSelector(state),
outOfScopeWorkItems: outOfScopeWorkItems(state)
}
}
}
const mapDispatchToProps = () => {
return (dispatch) => {
return {
launchWorkItemForm: (id) => {
if (id) {
dispatch(launchWorkItemForm(id));
}
}
}
}
}
export const ConnectedEpicRoadmapViewContent = connect(makeMapStateToProps, mapDispatchToProps)(EpicRoadmapViewContent);
export const EpicRoadmapView = () => {
const initialState: IEpicRoadmapState = {
} as IEpicRoadmapState;
const store = configureEpicRoadmapStore(initialState);
return (
<Provider store={store}>
<ConnectedEpicRoadmapViewContent />
</Provider>);
}

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

@ -0,0 +1,14 @@
.epic-selector-container {
display: flex;
margin-bottom: 5px;
margin-top: 5px;
}
.select-epic-label {
font-size: 15px;
margin-right: 5px;
}
.epic-selector-dropdown{
min-width: 500px;
}

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

@ -0,0 +1,75 @@
import { ComboBox, IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox';
import * as React from 'react';
import { connect } from 'react-redux';
import { WorkItem } from 'TFS/WorkItemTracking/Contracts';
import { selectEpic } from '../../../Common/redux/modules/SettingsState/SettingsStateActions';
import { IEpicRoadmapState } from '../../redux/contracts';
import './EpicSelector.scss';
export interface IEpicSelectorProps {
selectedId: number;
epics: WorkItem[];
selectEpic: (id: number) => void;
}
export class EpicSelectorContent extends React.Component<IEpicSelectorProps, {}> {
public render(): JSX.Element {
return (
<div className="epic-selector-container">
<ComboBox
className="epic-selector-dropdown"
ariaLabel="Select an Epic"
options={this._getOptions()}
defaultSelectedKey={this.props.selectedId + ""}
onChanged={this._epicSelectionChanged}
useComboBoxAsMenuWidth={true}
/>
</div>
);
}
private _getOptions(): IComboBoxOption[] {
const {
epics
} = this.props;
const sortedEpics = epics
.slice()
.sort((e1, e2) => {
const title1 = e1.fields["System.Title"] || "";
const title2 = e2.fields["System.Title"] || "";
return title1.localeCompare(title2);
});
return sortedEpics
.map(e => {
return {
key: e.id + "",
text: e.fields["System.Title"]
}
});
}
private _epicSelectionChanged = (option: IComboBoxOption) => {
this.props.selectEpic(Number(option.key));
}
}
const makeMapStateToProps = () => {
return (state: IEpicRoadmapState) => {
return {
epics: state.epicsAvailableState.epics,
selectedId: state.settingsState.lastEpicSelected
}
}
}
const mapDispatchToProps = (dispatch) => {
return {
selectEpic: (epicId) => {
dispatch(selectEpic(epicId));
}
}
}
export const EpicSelector = connect(makeMapStateToProps, mapDispatchToProps)(EpicSelectorContent)

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

@ -0,0 +1,49 @@
.timeline-dialog {
max-width: 60% !important;
.ms-Dialog--close {
overflow: auto;
}
}
.timeline-dialog .container {
max-height: 300px;
}
.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,129 @@
import { Button, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog';
import * as React from 'react';
import { IterationRenderer } from '../../../../Common/react/Components/IterationRenderer';
import { IterationDurationKind } from '../../../../Common/redux/Contracts/IIterationDuration';
import { EpicRoadmapGridViewSelector } from '../../../redux/selectors/EpicRoadmapGridViewSelector';
import { EpicRoadmapGridContent, IEpicRoadmapGridContentProps } from '../EpicRoadmapGrid';
import './RoadmapTimelineDialog.scss';
export interface IRoadmapTimelineDialogProps extends IEpicRoadmapGridContentProps {
clearOverrideIteration: (id: number) => void;
}
export class RoadmapTimelineDialog extends React.Component<IRoadmapTimelineDialogProps, {}> {
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._getId())}>Close</PrimaryButton>
</div>
</DialogFooter>
)
}
return (
<Dialog
hidden={false}
onDismiss={() => this.props.closeDetails(this._getId())}
dialogContentProps={
{
type: DialogType.close,
title: gridWorkItem.workItem.title
}
}
modalProps={
{
isBlocking: true,
containerClassName: "timeline-dialog"
}
}
>
{dialogDetails}
{footer}
</Dialog>
);
}
private _getChildrenFeatureTimelineGrid() {
const gridView = EpicRoadmapGridViewSelector(/*isSubGrid*/ true, this.props.rawState.workItemsToShowInfoFor[0])(this.props.rawState);
return (
<EpicRoadmapGridContent {...this.props} gridView={gridView} isSubGrid={true} />
);
}
private _getGridWorkItem() {
return this.props.gridView.workItems.filter(w => w.workItem.id === this._getId())[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 teamIterations={this.props.gridView.teamIterations} iteration={startIteration} />
</div>
<div className="custom-duration-iteration">
<IterationRenderer teamIterations={this.props.gridView.teamIterations} 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._getId())}>Close</PrimaryButton>
</div>
</div>
</div>
);
}
private _onClear = () => {
this.props.clearOverrideIteration(this._getId());
this.props.closeDetails(this._getId());
}
private _getId = () => {
return this.props.rawState.workItemsToShowInfoFor[0];
}
}

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

@ -0,0 +1,58 @@
import { applyMiddleware, combineReducers, compose, createStore, Store } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { iterationDisplayOptionsReducer } from '../../Common/redux/modules/IterationDisplayOptions/iterationDisplayOptionsReducer';
import { overrideIterationProgressReducer } from '../../Common/redux/modules/overrideIterationProgress/overrideIterationProgressReducer';
import { savedOverrideIterationsReducer } from '../../Common/redux/modules/OverrideIterations/overrideWorkItemIterationReducer';
import { progressAwareReducer } from '../../Common/redux/modules/ProgressAwareState/ProgressAwareStateReducer';
import { settingsStateReducer } from '../../Common/redux/modules/SettingsState/SettingsStateReducer';
import { IEpicRoadmapState } from './contracts';
import { backlogConfigurationReducer } from './modules/backlogconfiguration/backlogconfiguratonreducer';
import { teamIterationsReducer } from './modules/teamIterations/teamIterationReducer';
import { teamSettingsReducer } from './modules/teamsettings/teamsettingsreducer';
import { workItemMetadataReducer } from './modules/workItemMetadata/workItemMetadataReducer';
import { workItemsReducer } from './modules/workItems/workItemReducer';
import { showHideDetailsReducer } from '../../Common/redux/modules/ShowHideDetails/ShowHideDetailsReducer';
import { watchEpicRoadmapSagaActions } from './sagas/watchEpicRoadmapSagaActions';
import { highlightDependencyReducer } from '../../Common/redux/modules/HighlightDependencies/HighlightDependenciesModule';
import { FetchAllMetadata } from './sagas/FetchAllMetadata';
import { epicsAvailableReducer } from './modules/EpicsAvailable/EpicsAvailable';
export default function configureEpicRoadmapStore(
initialState: IEpicRoadmapState
): Store<IEpicRoadmapState> {
const sagaMonitor = window["__SAGA_MONITOR_EXTENSION__"] || undefined;
const sagaMiddleWare = createSagaMiddleware({sagaMonitor});
const middleware = applyMiddleware(sagaMiddleWare);
// 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 reducers = combineReducers({
backlogConfigurations: backlogConfigurationReducer,
teamIterations: teamIterationsReducer,
savedOverriddenIterations: savedOverrideIterationsReducer,
workItemsState: workItemsReducer,
workItemMetadata: workItemMetadataReducer,
teamSettings: teamSettingsReducer,
settingsState: settingsStateReducer,
iterationDisplayOptions: iterationDisplayOptionsReducer,
progress: progressAwareReducer,
workItemOverrideIteration: overrideIterationProgressReducer,
workItemsToShowInfoFor: showHideDetailsReducer,
highlightedDependency: highlightDependencyReducer ,
epicsAvailableState: epicsAvailableReducer
});
const store = createStore(
reducers,
initialState,
composeEnhancers(middleware));
sagaMiddleWare.run(watchEpicRoadmapSagaActions);
sagaMiddleWare.run(FetchAllMetadata);
return store;
}

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

@ -0,0 +1,30 @@
import { IIterationDisplayOptionsAwareState } from "../../Common/redux/modules/IterationDisplayOptions/IterationDisplayOptionsContracts";
import { IOverriddenIterationsAwareState, IWorkItemOverrideIterationAwareState } from '../../Common/redux/modules/OverrideIterations/overriddenIterationContracts';
import { IProgressAwareState } from "../../Common/redux/modules/ProgressAwareState/ProgressAwareStateContracts";
import { ISettingsAwareState } from "../../Common/redux/modules/SettingsState/SettingsStateContracts";
import { IProjectBacklogConfigurationAwareState } from "./modules/backlogconfiguration/backlogconfigurationcontracts";
import { ITeamIterationsAwareState } from "./modules/teamIterations/teamIterationsContracts";
import { ITeamSettingsAwareState } from "./modules/teamsettings/teamsettingscontracts";
import { IWorkItemMetadataAwareState } from "./modules/workItemMetadata/workItemMetadataContracts";
import { IEpicRoadmapWorkItemAwareState } from './modules/workItems/workItemContracts';
import { IShowWorkItemInfoAwareState } from "../../Common/redux/modules/ShowHideDetails/ShowHideDetailsContracts";
import { IHighlightDependenciesAwareState } from "../../Common/redux/modules/HighlightDependencies/HighlightDependenciesModule";
import { IEpicsAvailableAwareState } from "./modules/EpicsAvailable/EpicsAvailable";
export interface IEpicRoadmapState extends
IProjectBacklogConfigurationAwareState,
ITeamIterationsAwareState,
IOverriddenIterationsAwareState,
IEpicRoadmapWorkItemAwareState,
IWorkItemMetadataAwareState,
ITeamSettingsAwareState,
IIterationDisplayOptionsAwareState,
ISettingsAwareState,
IProgressAwareState,
IWorkItemOverrideIterationAwareState,
IShowWorkItemInfoAwareState,
IHighlightDependenciesAwareState,
IEpicsAvailableAwareState {
}
export const EpicsMetadataAvailable = "EpicsMetadataAvailable";

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

@ -0,0 +1,32 @@
import produce from 'immer';
import { WorkItem } from 'TFS/WorkItemTracking/Contracts';
import { ActionsUnion, createAction } from '../../../../Common/redux/Helpers/ActionHelper';
export interface IEpicsAvailableState {
epics: WorkItem[];
}
export interface IEpicsAvailableAwareState {
epicsAvailableState: IEpicsAvailableState;
}
export const EpicsAvailableType = "@@EpicsAvailable/EpicsAvailable";
export const EpicsAvailableCreator = {
epicsReceiveed: (workItems: WorkItem[]) =>
createAction(EpicsAvailableType, {
workItems
})
}
export type EpicsAvailableActions = ActionsUnion<typeof EpicsAvailableCreator>;
export function epicsAvailableReducer(state: IEpicsAvailableState, action: EpicsAvailableActions): IEpicsAvailableState {
state = state || { epics: undefined };
return produce(state, draft => {
switch (action.type) {
case EpicsAvailableType:
draft.epics = action.payload.workItems;
break;
}
});
}

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

@ -0,0 +1,13 @@
import { BacklogConfiguration } from "TFS/Work/Contracts";
import { createAction, ActionsUnion } from '../../../../Common/redux/Helpers/ActionHelper';
export const ProjectBacklogConfigurationReceivedType = "@@backlogconfiguration/ProjectBacklogConfigurationReceived";
export const ProjectBacklogConfigurationActionCreator = {
backlogConfigurationReceived: (projectId: string, backlogConfiguration: BacklogConfiguration) =>
createAction(ProjectBacklogConfigurationReceivedType, {
projectId,
backlogConfiguration
})
}
export type ProjectBacklogConfigurationActions = ActionsUnion<typeof ProjectBacklogConfigurationActionCreator>;

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

@ -0,0 +1,7 @@
import { BacklogConfiguration } from "TFS/Work/Contracts";
export type BacklogConfigurationMap = { [projectId: string]: BacklogConfiguration }
export interface IProjectBacklogConfigurationAwareState {
backlogConfigurations: BacklogConfigurationMap;
}

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

@ -0,0 +1,11 @@
import { createSelector } from "reselect";
import { getProjectId } from "../../../../Common/redux/Selectors/CommonSelectors";
import { IProjectBacklogConfigurationAwareState } from "./backlogconfigurationcontracts";
export const getBacklogConfigurationMap = (state: IProjectBacklogConfigurationAwareState) => state.backlogConfigurations;
export const backlogConfigurationForProjectSelector =
createSelector(
[getBacklogConfigurationMap, getProjectId],
(map, projectId) => map[projectId]
);

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

@ -0,0 +1,25 @@
import { BacklogConfigurationMap } from "./backlogconfigurationcontracts";
import produce from "immer";
import { ProjectBacklogConfigurationActions, ProjectBacklogConfigurationReceivedType } from "./backlogconfigurationactions";
export function backlogConfigurationReducer(state: BacklogConfigurationMap, action: ProjectBacklogConfigurationActions): BacklogConfigurationMap {
if(!state) {
state = {};
}
return produce(state, draft => {
const {
payload
} = action;
switch (action.type) {
case ProjectBacklogConfigurationReceivedType:
{
const {
projectId,
backlogConfiguration
} = payload;
draft[projectId] = backlogConfiguration;
draft[projectId].portfolioBacklogs.sort((pb1, pb2) => pb1.rank - pb2.rank);
}
}
});
}

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

@ -0,0 +1,25 @@
import { TeamIterationsMap } from './teamIterationsContracts';
import produce from "immer";
import { TeamIterationsActions, TeamIterationsReceivedType } from './teamIterationsActions';
import { compareIteration } from '../../../../Common/redux/Helpers/iterationComparer';
export function teamIterationsReducer(state: TeamIterationsMap, action: TeamIterationsActions): TeamIterationsMap {
if(!state) {
state = {};
}
return produce(state, draft => {
const {
payload
} = action;
switch (action.type) {
case TeamIterationsReceivedType:
{
const {
teamId,
teamIterations
} = payload;
draft[teamId] = teamIterations.sort(compareIteration);
}
}
});
}

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

@ -0,0 +1,10 @@
import { createSelector } from "reselect";
import { getTeamId } from '../../../../Common/redux/Selectors/CommonSelectors';
import { ITeamIterationsAwareState } from "./teamIterationsContracts";
export const getTeamIterationsMap = (state: ITeamIterationsAwareState) => state.teamIterations;
export const teamIterationsSelector = createSelector(
[getTeamIterationsMap, getTeamId],
(map, teamId) => map[teamId]
);

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

@ -0,0 +1,13 @@
import { ActionsUnion, createAction } from "../../../../Common/redux/Helpers/ActionHelper";
import { TeamSettingsIteration } from "TFS/Work/Contracts";
export const TeamIterationsReceivedType = "@@teamiterations/TeamIterationsReceived";
export const TeamIterationsActionCreator = {
teamIterationsReceived: (teamId: string, teamIterations: TeamSettingsIteration[]) =>
createAction(TeamIterationsReceivedType, {
teamId,
teamIterations
})
}
export type TeamIterationsActions = ActionsUnion<typeof TeamIterationsActionCreator>;

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше