Compare commits

...

2 Commits

7 changed files with 101 additions and 48 deletions

View File

@ -2,7 +2,7 @@
"manifestVersion": 1,
"id": "enhanced-sprint-history",
"publisher": "JamesSkemp",
"version": "0.0.82",
"version": "0.0.99",
"name": "Enhanced Sprint History",
"description": "Azure DevOps Extension",
"categories": [
@ -30,12 +30,12 @@
},
"links": {
"learn": {
"uri": "https://git.ebacher-skemp.com/strivinglife/enhanced-sprint-history"
"uri": "https://git.ebacher-skemp.com/azure-devops/enhanced-sprint-history"
}
},
"repository": {
"type": "git",
"uri": "https://git.ebacher-skemp.com/strivinglife/enhanced-sprint-history"
"uri": "https://git.ebacher-skemp.com/azure-devops/enhanced-sprint-history"
},
"files": [
{

View File

@ -1 +1 @@
Enhanced Sprint History supports viewing a sprint iteration over time.
Enhanced Sprint History supports viewing changes to user stories in an iteration over time.

View File

@ -14,7 +14,7 @@
},
"repository": {
"type": "git",
"url": "https://git.ebacher-skemp.com/strivinglife/enhanced-sprint-history.git"
"url": "https://git.ebacher-skemp.com/azure-devops/enhanced-sprint-history.git"
},
"keywords": [
"extensions",

View File

@ -8,6 +8,10 @@ h3 {
margin-top: 0;
}
.current-iteration, .current-state {
padding-left: 1em;
}
.enhanced-sprint-history {
font-size: $fontSizeM;
padding: 0 1em;

View File

@ -124,6 +124,8 @@ class HubContent extends React.Component<{}, IHubContentState> {
return (
<div>
<a href={workItem.url} target="_blank">{workItem.id}</a> : {workItem.title} ({workItem.storyPoints})
<div className="current-iteration">Current Iteration: {workItem.iterationPath}</div>
<div className="current-state">{workItem.state}</div>
</div>
)
});
@ -162,16 +164,14 @@ class HubContent extends React.Component<{}, IHubContentState> {
dismissOnSelect={true}
/>
<h2>Sprint History for {this.state.selectedTeamName} : {this.state.selectedTeamIterationName}</h2>
<h2>Iteration History for {this.state.selectedTeamName} : {this.state.selectedTeamIterationName}</h2>
{sprintDatesHeading(this.state.selectedTeamIteration)}
<h4>TODO User Stories</h4>
<h4>User Stories</h4>
<p>These stories are or have been in this iteration.</p>
{displayUserStories(this.state.workItems)}
<hr />
<h4>TODO User Story History</h4>
<h4>Iteration User Story History</h4>
<IterationHistoryDisplay iteration={this.state.selectedTeamIteration} workItemHistory={this.state.workItemsHistory} />
</Page>
);
@ -257,7 +257,14 @@ class HubContent extends React.Component<{}, IHubContentState> {
iterationId = this.teamIterations[0].id;
iterationName = this.teamIterations[0].name;
} else {
let currentIteration = this.teamIterations.find(i => i.attributes.timeFrame === 1);
let currentIteration: TeamSettingsIteration | undefined;
if (this.queryParamsTeamIteration) {
currentIteration = this.teamIterations.find(i => i.id === this.queryParamsTeamIteration);
}
if (!currentIteration) {
currentIteration = this.teamIterations.find(i => i.attributes.timeFrame === 1);
}
if (currentIteration) {
this.teamIterationSelection.select(this.teamIterations.indexOf(currentIteration));

View File

@ -0,0 +1,30 @@
table {
margin-bottom: 2em;
}
th, td {
padding: .25em .5em;
}
th.date {
width: 100px;
}
td {
vertical-align: top;
}
.story-points {
text-align: center;
&.total {
font-weight: bold;
&.decrease {
color: green;
}
&.increase {
color: red;
}
}
}

View File

@ -1,5 +1,6 @@
import * as React from "react";
import * as SDK from "azure-devops-extension-sdk";
import "./IterationHistoryDisplay.scss";
import { TeamSettingsIteration } from "azure-devops-extension-api/Work";
import { IHubWorkItemHistory, IHubWorkItemIterationRevisions, ITypedWorkItem, ITypedWorkItemWithRevision } from "./HubInterfaces";
import { getFlattenedRelevantRevisions, getIterationRelevantWorkItems, getTypedWorkItem } from "./HubUtils";
@ -9,24 +10,24 @@ export interface IterationHistoryDisplayProps {
workItemHistory: IHubWorkItemHistory[];
}
interface State {}
interface State {
totalStoryPoints: number;
}
export class IterationHistoryDisplay extends React.Component<IterationHistoryDisplayProps, State> {
constructor(props: IterationHistoryDisplayProps) {
super(props);
this.state = {};
this.state = {
totalStoryPoints: 0
};
}
public render(): JSX.Element {
const selectedIterationPath = this.props.iteration ? this.props.iteration.path : undefined;
const asdf: IHubWorkItemIterationRevisions[] = this.props.workItemHistory.map(wiHistory => {
const iterationWorkItemRevisions: IHubWorkItemIterationRevisions[] = this.props.workItemHistory.map(wiHistory => {
const typedWorkItems: ITypedWorkItem[] = wiHistory.revisions.map(workItem => getTypedWorkItem(workItem));
console.log(wiHistory);
console.log(`Typed Work Items for ${wiHistory.id}:`);
console.table(typedWorkItems);
const firstRevision = selectedIterationPath ? typedWorkItems.find(wi => wi.iterationPath === selectedIterationPath) : undefined;
return {
@ -39,7 +40,16 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
function getChangedWorkItems(workItems: ITypedWorkItem[]): ITypedWorkItem[] {
return workItems
.filter((wi, i, arr) => i === arr.findIndex((twi) => wi.iterationPath === twi.iterationPath && isWorkItemClosed(wi) === isWorkItemClosed(twi) && wi.storyPoints === twi.storyPoints && wi.id === twi.id))
.filter((wi, i, array) => {
if (i === 0) {
return true;
}
const previousItem = array[i-1];
if (wi.id !== previousItem.id || isWorkItemClosed(wi) !== isWorkItemClosed(previousItem) || wi.storyPoints !== previousItem.storyPoints) {
return true;
}
return false;
})
.sort((a, b) => a.changedDateFull === b.changedDateFull ? 0 : a.changedDateFull > b.changedDateFull ? 1 : -1);
}
@ -71,7 +81,7 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
returnData.change = 'Closed';
return returnData;
} else if (isWorkItemClosed(lastRevision)) {
returnData.change = 'Re-opened';
returnData.change = 'Reopened';
return returnData;
}
}
@ -79,12 +89,6 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
returnData.change = 'Story Points Changed';
return returnData;
}
console.groupCollapsed(currentIndex);
console.log(previousWorkItemRevisions);
console.log(workItem);
console.log(currentIndex);
console.log(allWorkItems);
console.groupEnd();
returnData.change = 'unknown';
return returnData;
}
@ -93,58 +97,66 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
return workItem.state === 'Closed';
}
let totalStoryPoints = 0;
return (
<div>
<div>TODO iteration</div>
<pre>
{JSON.stringify(this.props.iteration, null, 2)}
</pre>
<div>TODO filtered work items</div>
<table>
<thead>
<tr>
<th>Date</th>
<th className="date">Date</th>
<th>User Story</th>
<th>Change</th>
<th>Story Points</th>
<th>Story Points</th>
<th>Remaining</th>
<th className="story-points">Story Points<br />Added</th>
<th className="story-points">Story Points<br />Removed</th>
<th className="story-points">Remaining</th>
</tr>
</thead>
<tbody>
{
getChangedWorkItems(getFlattenedRelevantRevisions(asdf)).map((wi, i, a) => {
getChangedWorkItems(getFlattenedRelevantRevisions(iterationWorkItemRevisions)).map((wi, i, a) => {
const workItemChange = getWorkItemChange(wi, i, a);
const storyClosed = isWorkItemClosed(wi);
let addedStoryPoints = 0;
let subtractedStoryPoints = 0;
if (storyClosed) {
subtractedStoryPoints = wi.storyPoints;
} else if (workItemChange.change === 'Removed') {
if (workItemChange.change === 'Removed') {
subtractedStoryPoints = wi.storyPoints;
} else if (workItemChange.change === 'Added') {
addedStoryPoints = wi.storyPoints;
} else if (workItemChange.change === 'Story Points Changed') {
addedStoryPoints = wi.storyPoints;
subtractedStoryPoints = workItemChange.lastRevision?.storyPoints ?? 0;
} else if (workItemChange.change === 'Reopened') {
addedStoryPoints = wi.storyPoints;
} else if (storyClosed) {
subtractedStoryPoints = wi.storyPoints;
}
let changeCharacterCode = 160;
if (addedStoryPoints > subtractedStoryPoints) {
changeCharacterCode = 8593; //8599;
} else if (addedStoryPoints < subtractedStoryPoints) {
changeCharacterCode = 8595; //8600
}
let updatedTotalStoryPoints = addedStoryPoints - subtractedStoryPoints;
totalStoryPoints += updatedTotalStoryPoints;
const totalStoryPointsClass = 'story-points total' + (updatedTotalStoryPoints > 0 ? ' increase' : updatedTotalStoryPoints < 0 ? ' decrease' : '');
return (
<tr>
<td>{wi.changedDateFull.toLocaleString()}</td>
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a></td>
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a><br />{wi.title}</td>
<td>{workItemChange.change}</td>
<td>{addedStoryPoints !== 0 && workItemChange.change !== 'Story Points Changed' ? addedStoryPoints : ''}</td>
<td>{subtractedStoryPoints !== 0 && workItemChange.change !== 'Story Points Changed' ? subtractedStoryPoints : ''}</td>
<td>{JSON.stringify(workItemChange)}</td>
<td className="story-points increase">{addedStoryPoints !== 0 || workItemChange.change === 'Story Points Changed' ? addedStoryPoints : ''}</td>
<td className="story-points decrease">{subtractedStoryPoints !== 0 || workItemChange.change === 'Story Points Changed' || storyClosed || workItemChange.change === 'Removed' ? subtractedStoryPoints : ''}</td>
<td className={totalStoryPointsClass}>{totalStoryPoints} {String.fromCharCode(changeCharacterCode)}</td>
</tr>
);
})
}
</tbody>
</table>
<pre>
{JSON.stringify(getChangedWorkItems(getFlattenedRelevantRevisions(asdf)), null, 2)}
</pre>
</div>
);
}