Compare commits
2 Commits
6be866f393
...
2aa72cc194
Author | SHA1 | Date |
---|---|---|
James Skemp | 2aa72cc194 | |
James Skemp | bcc1fc450c |
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -8,6 +8,10 @@ h3 {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.current-iteration, .current-state {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.enhanced-sprint-history {
|
||||
font-size: $fontSizeM;
|
||||
padding: 0 1em;
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue