Compare commits

...

3 Commits

6 changed files with 124 additions and 95 deletions

View File

@ -2,7 +2,7 @@
"manifestVersion": 1,
"id": "iteration-work-items-hub",
"publisher": "JamesSkemp",
"version": "1.0.115",
"version": "1.0.118",
"name": "Iteration Work Items Hub",
"description": "Iteration Work Items Hub supports viewing all items within a Team Iteration in Azure DevOps grouped by type and board state.",
"categories": [

135
package-lock.json generated
View File

@ -9,9 +9,9 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"azure-devops-extension-api": "^2.226.0",
"azure-devops-extension-api": "^4.236.0",
"azure-devops-extension-sdk": "^3.1.3",
"azure-devops-ui": "^2.236.0",
"azure-devops-ui": "^2.237.0",
"react": "~16.13.1",
"react-dom": "~16.13.1"
},
@ -30,7 +30,7 @@
"ts-loader": "~5.2.2",
"typescript": "^3.9.10",
"webpack": "^5.91.0",
"webpack-cli": "^4.10.0"
"webpack-cli": "^5.1.4"
}
},
"node_modules/@babel/code-frame": {
@ -536,34 +536,42 @@
}
},
"node_modules/@webpack-cli/configtest": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz",
"integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
"integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
"dev": true,
"engines": {
"node": ">=14.15.0"
},
"peerDependencies": {
"webpack": "4.x.x || 5.x.x",
"webpack-cli": "4.x.x"
"webpack": "5.x.x",
"webpack-cli": "5.x.x"
}
},
"node_modules/@webpack-cli/info": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz",
"integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
"integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
"dev": true,
"dependencies": {
"envinfo": "^7.7.3"
"engines": {
"node": ">=14.15.0"
},
"peerDependencies": {
"webpack-cli": "4.x.x"
"webpack": "5.x.x",
"webpack-cli": "5.x.x"
}
},
"node_modules/@webpack-cli/serve": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz",
"integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
"integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
"dev": true,
"engines": {
"node": ">=14.15.0"
},
"peerDependencies": {
"webpack-cli": "4.x.x"
"webpack": "5.x.x",
"webpack-cli": "5.x.x"
},
"peerDependenciesMeta": {
"webpack-dev-server": {
@ -1087,21 +1095,14 @@
"peer": true
},
"node_modules/azure-devops-extension-api": {
"version": "2.226.0",
"resolved": "https://registry.npmjs.org/azure-devops-extension-api/-/azure-devops-extension-api-2.226.0.tgz",
"integrity": "sha512-cbO1uE/Ropo85ZpCIHKFR9T4l5cPhyDyWaKKgs+V+urrdSbYehKkFmjOr1jU4xdc52QoDwWJH3P26Jep+s9bTg==",
"version": "4.236.0",
"resolved": "https://registry.npmjs.org/azure-devops-extension-api/-/azure-devops-extension-api-4.236.0.tgz",
"integrity": "sha512-Xj18MxZrTZx8G/N4a9nR3NdAPLA78q2tyc8FWC41mwwbD9THApjT00OVlZGdMd18FvVfZZpKvTSFJpN/6SIfsw==",
"dependencies": {
"azure-devops-extension-sdk": "~2.0.11",
"whatwg-fetch": "~3.0.0"
}
},
"node_modules/azure-devops-extension-api/node_modules/azure-devops-extension-sdk": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/azure-devops-extension-sdk/-/azure-devops-extension-sdk-2.0.11.tgz",
"integrity": "sha512-9Hv4IeKpXR7EbaQn75Qz78rp0nv88XzRTF20E2oHlEFc+TRerekQnJjdrTHChoVvPvsWL/NAEwlqINU60U7neg==",
"dependencies": {
"es6-object-assign": "^1.1.0",
"es6-promise": "^4.2.5"
},
"peerDependencies": {
"azure-devops-extension-sdk": "^2 || ^3 || ^4"
}
},
"node_modules/azure-devops-extension-sdk": {
@ -1124,16 +1125,16 @@
}
},
"node_modules/azure-devops-ui": {
"version": "2.236.0",
"resolved": "https://registry.npmjs.org/azure-devops-ui/-/azure-devops-ui-2.236.0.tgz",
"integrity": "sha512-MlmxjRYJXPOjlyJ6MmDw4lcE6n0LUs3v7NO+Im67vhl7YdADQyaXhglRlm+RIR8utHo+Jj5dbY3yDichS5LfVQ==",
"version": "2.238.0",
"resolved": "https://registry.npmjs.org/azure-devops-ui/-/azure-devops-ui-2.238.0.tgz",
"integrity": "sha512-Rr07EgFVxa6EaYQV3K0V6We9HBGNw1/XObr/Eboao+kYZ9Md2KLwLaWE1Tbqy+aoLe6tmGtMNtIwuhO1ML1pUQ==",
"dependencies": {
"array.prototype.find": "~2.0.4",
"es6-object-assign": "~1.1.0",
"es6-promise": "~4.2.5",
"es6-string-polyfills": "~1.0.0",
"intersection-observer": "~0.5.1",
"tslib": "~1.10.0"
"tslib": "~2.6.2"
},
"peerDependencies": {
"react": "^16.8.1",
@ -2476,9 +2477,9 @@
}
},
"node_modules/envinfo": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.12.0.tgz",
"integrity": "sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
"integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
"dev": true,
"bin": {
"envinfo": "dist/cli.js"
@ -3977,12 +3978,12 @@
}
},
"node_modules/interpret": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
"integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
"dev": true,
"engines": {
"node": ">= 0.10"
"node": ">=10.13.0"
}
},
"node_modules/intersection-observer": {
@ -6331,15 +6332,15 @@
}
},
"node_modules/rechoir": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz",
"integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==",
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
"integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
"dev": true,
"dependencies": {
"resolve": "^1.9.0"
"resolve": "^1.20.0"
},
"engines": {
"node": ">= 0.10"
"node": ">= 10.13.0"
}
},
"node_modules/redent": {
@ -7915,9 +7916,9 @@
}
},
"node_modules/tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tunnel": {
"version": "0.0.6",
@ -8337,44 +8338,42 @@
}
},
"node_modules/webpack-cli": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
"integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^1.2.0",
"@webpack-cli/info": "^1.5.0",
"@webpack-cli/serve": "^1.7.0",
"@webpack-cli/configtest": "^2.1.1",
"@webpack-cli/info": "^2.0.2",
"@webpack-cli/serve": "^2.0.5",
"colorette": "^2.0.14",
"commander": "^7.0.0",
"commander": "^10.0.1",
"cross-spawn": "^7.0.3",
"envinfo": "^7.7.3",
"fastest-levenshtein": "^1.0.12",
"import-local": "^3.0.2",
"interpret": "^2.2.0",
"rechoir": "^0.7.0",
"interpret": "^3.1.1",
"rechoir": "^0.8.0",
"webpack-merge": "^5.7.3"
},
"bin": {
"webpack-cli": "bin/cli.js"
},
"engines": {
"node": ">=10.13.0"
"node": ">=14.15.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "4.x.x || 5.x.x"
"webpack": "5.x.x"
},
"peerDependenciesMeta": {
"@webpack-cli/generators": {
"optional": true
},
"@webpack-cli/migrate": {
"optional": true
},
"webpack-bundle-analyzer": {
"optional": true
},
@ -8384,12 +8383,12 @@
}
},
"node_modules/webpack-cli/node_modules/commander": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz",
"integrity": "sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"engines": {
"node": ">= 10"
"node": ">=14"
}
},
"node_modules/webpack-merge": {

View File

@ -2,16 +2,6 @@
"name": "iteration-work-items-hub",
"version": "1.0.0",
"description": "Iteration Work Items Hub",
"keywords": [
"extensions",
"Azure DevOps",
"Visual Studio Team Services"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.ebacher-skemp.com/azure-devops/iteration-work-items-hub.git"
},
"scripts": {
"clean": "rimraf ./dist",
"compile": "npm run clean && webpack --mode production",
@ -22,11 +12,21 @@
"package-extension": "tfx extension create --manifest-globs azure-devops-extension.json src/Samples/**/*.json",
"publish-extension": "tfx extension publish --manifest-globs azure-devops-extension.json src/Samples/**/*.json"
},
"repository": {
"type": "git",
"url": "https://git.ebacher-skemp.com/azure-devops/iteration-work-items-hub.git"
},
"keywords": [
"extensions",
"Azure DevOps",
"Visual Studio Team Services"
],
"author": "James Skemp",
"license": "MIT",
"dependencies": {
"azure-devops-extension-api": "^2.226.0",
"azure-devops-extension-api": "^4.236.0",
"azure-devops-extension-sdk": "^3.1.3",
"azure-devops-ui": "^2.236.0",
"azure-devops-ui": "^2.237.0",
"react": "~16.13.1",
"react-dom": "~16.13.1"
},
@ -45,6 +45,6 @@
"ts-loader": "~5.2.2",
"typescript": "^3.9.10",
"webpack": "^5.91.0",
"webpack-cli": "^4.10.0"
"webpack-cli": "^5.1.4"
}
}

View File

@ -18,3 +18,7 @@
margin-bottom: 0;
}
}
p.iteration-dates {
margin-top: 0;
}

View File

@ -27,7 +27,8 @@ interface IHubContentState {
teamIterations: TeamSettingsIteration[];
selectedTeam: string;
selectedTeamName: string;
selectedTeamIteration: string;
selectedTeamIteration: TeamSettingsIteration | undefined;
selectedTeamIterationId: string;
selectedTeamIterationName: string;
iterationWorkItems?: IterationWorkItems;
taskboardWorkItemColumns: TaskboardWorkItemColumn[];
@ -67,7 +68,8 @@ class HubContent extends React.Component<{}, IHubContentState> {
teamIterations: [],
selectedTeam: '',
selectedTeamName: '',
selectedTeamIteration: '',
selectedTeamIteration: undefined,
selectedTeamIterationId: '',
selectedTeamIterationName: '',
taskboardWorkItemColumns: [],
taskboardColumns: [],
@ -109,6 +111,16 @@ class HubContent extends React.Component<{}, IHubContentState> {
}
}
function sprintDatesHeading(selectedTeamIteration: TeamSettingsIteration | undefined): JSX.Element | null {
if (selectedTeamIteration) {
return (
<p className="iteration-dates">{selectedTeamIteration.attributes.startDate.toLocaleDateString(undefined, { timeZone: 'UTC' })} - {selectedTeamIteration.attributes.finishDate.toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
);
} else {
return null;
}
}
/**
* Returns all work items (user stories, tasks, bugs) as a custom object for later display.
*/
@ -120,7 +132,8 @@ class HubContent extends React.Component<{}, IHubContentState> {
url: workItem.url.replace('/_apis/wit/workItems/', '/_workitems/edit/'),
boardColumn: workItem.fields['System.BoardColumn'],
state: workItem.fields['System.State'],
type: workItem.fields['System.WorkItemType']
type: workItem.fields['System.WorkItemType'],
storyPoints: workItem.fields['Microsoft.VSTS.Scheduling.StoryPoints'] ?? 0
};
const taskboardColumn = this.state.taskboardWorkItemColumns.find(wic => wic.workItemId === workItem.id);
@ -150,7 +163,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
const workItems = columnMatchingWorkItems.map(workItem => {
return (
<li key={workItem.id}>
<a href={workItem.url}>{workItem.id}</a> : {workItem.title} - {workItem.assignedTo}
<a href={workItem.url}>{workItem.id}</a> : {workItem.title} {workItem.storyPoints !== 0 && <span>({workItem.storyPoints})</span>} - {workItem.assignedTo}
</li>
);
});
@ -205,6 +218,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
</div>
{this.state.selectedTeamIterationName && <h2 id="selected-iteration">Work Items for {this.state.selectedTeamName} : {this.state.selectedTeamIterationName}</h2>}
{sprintDatesHeading(this.state.selectedTeamIteration)}
{sortedWorkItems}
</Page>
@ -281,11 +295,13 @@ class HubContent extends React.Component<{}, IHubContentState> {
}
this.setState({ teamIterations: this.teamIterations });
let iteration;
let iterationId = "";
let iterationName = "";
if (this.teamIterations.length === 1) {
this.teamIterationSelection.select(0);
iteration = this.teamIterations[0];
iterationId = this.teamIterations[0].id;
iterationName = this.teamIterations[0].name;
} else {
@ -300,6 +316,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
if (currentIteration) {
this.teamIterationSelection.select(this.teamIterations.indexOf(currentIteration));
iteration = currentIteration;
iterationId = currentIteration.id;
iterationName = currentIteration.name;
}
@ -307,7 +324,10 @@ class HubContent extends React.Component<{}, IHubContentState> {
if (iterationId !== '') {
this.setState({
selectedTeamIteration: iterationId
selectedTeamIteration: iteration
});
this.setState({
selectedTeamIterationId: iterationId
});
this.setState({
selectedTeamIterationName: iterationName
@ -321,7 +341,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
const teamContext = { projectId: this.state.project, teamId: this.state.selectedTeam, project: "", team: "" };
const workClient = getClient(WorkRestClient);
this.iterationWorkItems = await workClient.getIterationWorkItems(teamContext, this.state.selectedTeamIteration);
this.iterationWorkItems = await workClient.getIterationWorkItems(teamContext, this.state.selectedTeamIterationId);
if (!this.iterationWorkItems || this.iterationWorkItems.workItemRelations.length === 0) {
this.showToast('No work items found for this iteration.');
@ -354,7 +374,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
let manuallyGenerateTaskboardWorkItemColumns = false;
try {
const workItemColumns = await workClient.getWorkItemColumns(teamContext, this.state.selectedTeamIteration);
const workItemColumns = await workClient.getWorkItemColumns(teamContext, this.state.selectedTeamIterationId);
this.setState({ taskboardWorkItemColumns: workItemColumns });
} catch (ex) {
this.showToast('No work item columns were found for this team. These will be generated automatically from the work items.');
@ -388,7 +408,10 @@ class HubContent extends React.Component<{}, IHubContentState> {
selectedTeamName: item.text ?? ''
});
this.setState({
selectedTeamIteration: ''
selectedTeamIteration: undefined
});
this.setState({
selectedTeamIterationId: ''
});
this.setState({
selectedTeamIterationName: ''
@ -399,7 +422,10 @@ class HubContent extends React.Component<{}, IHubContentState> {
private handleSelectTeamIteration = (_event: React.SyntheticEvent<HTMLElement>, item: IListBoxItem<{}>): void => {
this.setState({
selectedTeamIteration: item.id
selectedTeamIteration: this.state.teamIterations.find(ti => ti.id === item.id)
});
this.setState({
selectedTeamIterationId: item.id
});
this.setState({
selectedTeamIterationName: item.text ?? ''
@ -412,7 +438,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
const navService = await SDK.getService<IHostNavigationService>(CommonServiceIds.HostNavigationService);
const hash = await navService.getQueryParams();
return { queryTeam: hash['selectedTeam'], queryTeamIteration: hash['selectedTeamIteration'] };
return { queryTeam: hash['selectedTeam'], queryTeamIteration: hash['selectedTeamIterationId'] };
}
private showToast = async (message: string): Promise<void> => {
@ -425,7 +451,7 @@ class HubContent extends React.Component<{}, IHubContentState> {
private updateQueryParams = async () => {
const navService = await SDK.getService<IHostNavigationService>(CommonServiceIds.HostNavigationService);
navService.setQueryParams({ selectedTeam: "" + this.state.selectedTeam, selectedTeamIteration: this.state.selectedTeamIteration });
navService.setQueryParams({ selectedTeam: "" + this.state.selectedTeam, selectedTeamIterationId: this.state.selectedTeamIterationId });
navService.setDocumentTitle("" + this.state.selectedTeamName + " : " + this.state.selectedTeamIterationName + " - Iteration Work Items");
}
}

View File

@ -43,10 +43,10 @@ module.exports = {
},
{
test: /\.html$/,
loader: "file-loader"
loader: "asset/resource"
},
{
test: /\.woff$/,
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/inline'
}
]