Compare commits

...

4 Commits

4 changed files with 170 additions and 74 deletions

View File

@ -2,7 +2,7 @@
"manifestVersion": 1,
"id": "enhanced-sprint-history",
"publisher": "JamesSkemp",
"version": "0.0.62",
"version": "0.0.82",
"name": "Enhanced Sprint History",
"description": "Azure DevOps Extension",
"categories": [

View File

@ -10,13 +10,14 @@ import { Page } from "azure-devops-ui/Page";
import { showRootComponent } from "../Common";
import { IListBoxItem } from "azure-devops-ui/ListBox";
import { WorkItem, WorkItemReference, WorkItemTrackingRestClient, WorkItemType, WorkItemUpdate } from "azure-devops-extension-api/WorkItemTracking";
import { IterationWorkItems, TaskboardColumn, TaskboardColumns, TaskboardWorkItemColumn, TeamSettingsIteration, WorkRestClient } from "azure-devops-extension-api/Work";
import { WorkItem, WorkItemReference, WorkItemTrackingRestClient, WorkItemType } from "azure-devops-extension-api/WorkItemTracking";
import { IterationWorkItems, TaskboardColumn, TaskboardWorkItemColumn, TeamSettingsIteration, WorkRestClient } from "azure-devops-extension-api/Work";
import { CoreRestClient, WebApiTeam } from "azure-devops-extension-api/Core";
import { Dropdown } from "azure-devops-ui/Dropdown";
import { ListSelection } from "azure-devops-ui/List";
import { IHubWorkItemHistory, IHubWorkItemIterationRevisions, ITypedWorkItem } from "./HubInterfaces";
import { getFlattenedRelevantRevisions, getIterationRelevantWorkItems, getTypedWorkItem } from "./HubUtils";
import { IHubWorkItemHistory } from "./HubInterfaces";
import { getTypedWorkItem } from "./HubUtils";
import { IterationHistoryDisplay } from "./IterationHistoryDisplay";
interface IHubContentState {
project: string;
@ -122,7 +123,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
const workItemDisplay = typedWorkItems.map(workItem => {
return (
<div>
<a href={workItem.url}>{workItem.id}</a> : {workItem.title} ({workItem.storyPoints})
<a href={workItem.url} target="_blank">{workItem.id}</a> : {workItem.title} ({workItem.storyPoints})
</div>
)
});
@ -134,58 +135,6 @@ class HubContent extends React.Component<{}, IHubContentState> {
);
}
function displayUserStoryHistory(workItemHistory: IHubWorkItemHistory[], selectedIterationPath: string | undefined) {
const asdf: IHubWorkItemIterationRevisions[] = 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;
/*if (firstRevision) {
console.log(firstRevision.iterationPath);
console.log(firstRevision.changedDate);
console.log(firstRevision.storyPoints);
}*/
return {
id: wiHistory.id,
iterationPath: selectedIterationPath,
firstRevision: firstRevision,
relevantRevisions: selectedIterationPath ? getIterationRelevantWorkItems(typedWorkItems, selectedIterationPath) : []
};
});
if (asdf?.length > 0) {
console.log(getFlattenedRelevantRevisions(asdf));
asdf.forEach(element => {
console.groupCollapsed(element.id);
console.log(element.firstRevision);
console.table(element.relevantRevisions);
console.groupEnd();
});
}
const workItemHistoryDisplay = workItemHistory.map(wiHistory => {
return (
<div>
{wiHistory.id}<br />
<pre>
{JSON.stringify(wiHistory.revisions, null, 2)}
</pre>
<hr />
</div>
)
});
return (
<React.Fragment>
{workItemHistoryDisplay}
</React.Fragment>
);
}
return (
<Page className="enhanced-sprint-history flex-grow">
<Header title="Enhanced Sprint History"
@ -220,20 +169,10 @@ class HubContent extends React.Component<{}, IHubContentState> {
<h4>TODO User Stories</h4>
{displayUserStories(this.state.workItems)}
<h4>TODO User Stories Dump</h4>
<pre>{
JSON.stringify(this.state.workItems, null, 2)
}</pre>
<hr />
<h4>TODO User Story History</h4>
{displayUserStoryHistory(this.state.workItemsHistory, this.state.selectedTeamIteration?.path)}
<h4>TODO User Story History Dump</h4>
<pre>{
JSON.stringify(this.state.workItemsHistory, null, 2)
}</pre>
<IterationHistoryDisplay iteration={this.state.selectedTeamIteration} workItemHistory={this.state.workItemsHistory} />
</Page>
);
}

View File

@ -2,16 +2,22 @@ import { WorkItem } from "azure-devops-extension-api/WorkItemTracking";
export interface ITypedWorkItem {
id: number;
title: any;
title: string;
url: string;
iterationPath: any;
iterationPath: string;
storyPoints: number;
changedDate: any;
changedDateFull: any;
state: any;
changedDate: string;
changedDateFull: Date;
state: string;
revision: number;
}
export interface ITypedWorkItemWithRevision {
workItem: ITypedWorkItem;
lastRevision: ITypedWorkItem | undefined;
change: string;
}
export interface IHubWorkItemHistory {
id: number;
revisions: WorkItem[];

View File

@ -0,0 +1,151 @@
import * as React from "react";
import * as SDK from "azure-devops-extension-sdk";
import { TeamSettingsIteration } from "azure-devops-extension-api/Work";
import { IHubWorkItemHistory, IHubWorkItemIterationRevisions, ITypedWorkItem, ITypedWorkItemWithRevision } from "./HubInterfaces";
import { getFlattenedRelevantRevisions, getIterationRelevantWorkItems, getTypedWorkItem } from "./HubUtils";
export interface IterationHistoryDisplayProps {
iteration: TeamSettingsIteration | undefined;
workItemHistory: IHubWorkItemHistory[];
}
interface State {}
export class IterationHistoryDisplay extends React.Component<IterationHistoryDisplayProps, State> {
constructor(props: IterationHistoryDisplayProps) {
super(props);
this.state = {};
}
public render(): JSX.Element {
const selectedIterationPath = this.props.iteration ? this.props.iteration.path : undefined;
const asdf: 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 {
id: wiHistory.id,
iterationPath: selectedIterationPath,
firstRevision: firstRevision,
relevantRevisions: selectedIterationPath ? getIterationRelevantWorkItems(typedWorkItems, selectedIterationPath) : []
};
});
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))
.sort((a, b) => a.changedDateFull === b.changedDateFull ? 0 : a.changedDateFull > b.changedDateFull ? 1 : -1);
}
function getWorkItemChange(workItem: ITypedWorkItem, currentIndex: number, allWorkItems: ITypedWorkItem[]): ITypedWorkItemWithRevision {
const previousWorkItemRevisions = allWorkItems
.slice(0, currentIndex)
.filter(wi => wi.id === workItem.id)
.sort((a, b) => a.revision === b.revision ? 0 : a.revision < b.revision ? 1 : -1);
let returnData: ITypedWorkItemWithRevision = {
workItem: workItem,
lastRevision: undefined,
change: ''
};
if (previousWorkItemRevisions.length === 0) {
returnData.change = 'Added';
return returnData;
}
const lastRevision = previousWorkItemRevisions[0];
returnData.lastRevision = previousWorkItemRevisions[0];
if (lastRevision.iterationPath !== workItem.iterationPath) {
returnData.change = workItem.iterationPath === selectedIterationPath ? 'Added' : 'Removed';
return returnData;
}
if (isWorkItemClosed(lastRevision) !== isWorkItemClosed(workItem)) {
if (isWorkItemClosed(workItem)) {
returnData.change = 'Closed';
return returnData;
} else if (isWorkItemClosed(lastRevision)) {
returnData.change = 'Re-opened';
return returnData;
}
}
if (lastRevision.storyPoints !== workItem.storyPoints) {
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;
}
function isWorkItemClosed(workItem: ITypedWorkItem): boolean {
return workItem.state === 'Closed';
}
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>User Story</th>
<th>Change</th>
<th>Story Points</th>
<th>Story Points</th>
<th>Remaining</th>
</tr>
</thead>
<tbody>
{
getChangedWorkItems(getFlattenedRelevantRevisions(asdf)).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') {
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;
}
return (
<tr>
<td>{wi.changedDateFull.toLocaleString()}</td>
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a></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>
</tr>
);
})
}
</tbody>
</table>
<pre>
{JSON.stringify(getChangedWorkItems(getFlattenedRelevantRevisions(asdf)), null, 2)}
</pre>
</div>
);
}
}