Epic rollup
This commit is contained in:
Родитель
86342174ad
Коммит
cc43e45aaa
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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)}>
|
||||
|
||||
</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)}>
|
||||
|
||||
</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"> </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"> </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)}
|
||||
>
|
||||
|
||||
</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>;
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче