Compare commits
4 Commits
916c4b92bb
...
6be866f393
Author | SHA1 | Date |
---|---|---|
James Skemp | 6be866f393 | |
James Skemp | b890a112b4 | |
James Skemp | 4fd804efda | |
James Skemp | 4ec6e1cf9c |
|
@ -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": [
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue