Skip to content

Commit 9b13082

Browse files
committed
Retrieves and caches state-map for the current Azure project.
(#3977)
1 parent 07e5cf7 commit 9b13082

File tree

2 files changed

+149
-5
lines changed

2 files changed

+149
-5
lines changed

Diff for: src/plus/integrations/providers/azure/azure.ts

+120-5
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import { Logger } from '../../../../system/logger';
2222
import type { LogScope } from '../../../../system/logger.scope';
2323
import { getLogScope } from '../../../../system/logger.scope';
2424
import { maybeStopWatch } from '../../../../system/stopwatch';
25-
import type { WorkItem } from './models';
25+
import type { AzureWorkItemState, AzureWorkItemStateCategory, WorkItem } from './models';
26+
import { azureWorkItemsStateCategoryToState, isClosedAzureWorkItemStateCategory } from './models';
2627

2728
export class AzureDevOpsApi implements Disposable {
2829
private readonly _disposable: Disposable;
30+
private _workItemStates: WorkItemStates = new WorkItemStates();
2931

3032
constructor(_container: Container) {
3133
this._disposable = configuration.onDidChangeAny(e => {
@@ -54,8 +56,7 @@ export class AzureDevOpsApi implements Disposable {
5456

5557
private resetCaches(): void {
5658
this._proxyAgent = null;
57-
// this._defaults.clear();
58-
// this._enterpriseVersions.clear();
59+
this._workItemStates.clear();
5960
}
6061

6162
@debug<AzureDevOpsApi['getIssueOrPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
@@ -86,15 +87,27 @@ export class AzureDevOpsApi implements Disposable {
8687
);
8788

8889
if (issueResult != null) {
90+
const issueType = issueResult.fields['System.WorkItemType'];
91+
const state = issueResult.fields['System.State'];
92+
const stateCategory = await this.getWorkItemStateCategory(
93+
issueType,
94+
state,
95+
provider,
96+
token,
97+
owner,
98+
repo,
99+
options,
100+
);
101+
89102
return {
90103
id: issueResult.id.toString(),
91104
type: 'issue',
92105
nodeId: issueResult.id.toString(),
93106
provider: provider,
94107
createdDate: new Date(issueResult.fields['System.CreatedDate']),
95108
updatedDate: new Date(issueResult.fields['System.ChangedDate']),
96-
state: issueResult.fields['System.State'] === 'Closed' ? 'closed' : 'opened',
97-
closed: issueResult.fields['System.State'] === 'Closed',
109+
state: azureWorkItemsStateCategoryToState(stateCategory),
110+
closed: isClosedAzureWorkItemStateCategory(stateCategory),
98111
title: issueResult.fields['System.Title'],
99112
url: issueResult._links.html.href,
100113
};
@@ -107,6 +120,60 @@ export class AzureDevOpsApi implements Disposable {
107120
}
108121
}
109122

123+
public async getWorkItemStateCategory(
124+
issueType: string,
125+
state: string,
126+
provider: Provider,
127+
token: string,
128+
owner: string,
129+
repo: string,
130+
options: {
131+
baseUrl: string;
132+
},
133+
): Promise<AzureWorkItemStateCategory | undefined> {
134+
const [projectName] = repo.split('/');
135+
const project = `${owner}/${projectName}`;
136+
const category = this._workItemStates.getStateCategory(project, issueType, state);
137+
if (category != null) return category;
138+
139+
const states = await this.retrieveWorkItemTypeStates(issueType, provider, token, owner, repo, options);
140+
this._workItemStates.saveTypeStates(project, issueType, states);
141+
142+
return this._workItemStates.getStateCategory(project, issueType, state);
143+
}
144+
145+
private async retrieveWorkItemTypeStates(
146+
workItemType: string,
147+
provider: Provider,
148+
token: string,
149+
owner: string,
150+
repo: string,
151+
options: {
152+
baseUrl: string;
153+
},
154+
): Promise<AzureWorkItemState[]> {
155+
const scope = getLogScope();
156+
const [projectName] = repo.split('/');
157+
158+
try {
159+
const issueResult = await this.request<{ value: AzureWorkItemState[]; count: number }>(
160+
provider,
161+
token,
162+
options?.baseUrl,
163+
`${owner}/${projectName}/_apis/wit/workItemTypes/${workItemType}/states`,
164+
{
165+
method: 'GET',
166+
},
167+
scope,
168+
);
169+
170+
return issueResult?.value ?? [];
171+
} catch (ex) {
172+
Logger.error(ex, scope);
173+
return [];
174+
}
175+
}
176+
110177
private async request<T>(
111178
provider: Provider,
112179
token: string,
@@ -227,3 +294,51 @@ export class AzureDevOpsApi implements Disposable {
227294
}
228295
}
229296
}
297+
298+
class WorkItemStates {
299+
private readonly _categories = new Map<string, AzureWorkItemStateCategory>();
300+
private readonly _types = new Map<string, AzureWorkItemState[]>();
301+
302+
// TODO@sergeibbb: we might need some logic for invalidating
303+
public getStateCategory(
304+
project: string,
305+
workItemType: string,
306+
stateName: string,
307+
): AzureWorkItemStateCategory | undefined {
308+
return this._categories.get(this.getStateKey(project, workItemType, stateName));
309+
}
310+
311+
public clear(): void {
312+
this._categories.clear();
313+
this._types.clear();
314+
}
315+
316+
public saveTypeStates(project: string, workItemType: string, states: AzureWorkItemState[]): void {
317+
this.clearTypeStates(project, workItemType);
318+
this._types.set(this.getTypeKey(project, workItemType), states);
319+
for (const state of states) {
320+
this._categories.set(this.getStateKey(project, workItemType, state.name), state.category);
321+
}
322+
}
323+
324+
public hasTypeStates(project: string, workItemType: string): boolean {
325+
return this._types.has(this.getTypeKey(project, workItemType));
326+
}
327+
328+
private clearTypeStates(project: string, workItemType: string): void {
329+
const states = this._types.get(this.getTypeKey(project, workItemType));
330+
if (states == null) return;
331+
for (const state of states) {
332+
this._categories.delete(this.getStateKey(project, workItemType, state.name));
333+
}
334+
}
335+
336+
private getStateKey(project: string, workItemType: string, stateName: string): string {
337+
// By stringifying the pair as JSON we make sure that all possible special characters are escaped
338+
return JSON.stringify([project, workItemType, stateName]);
339+
}
340+
341+
private getTypeKey(project: string, workItemType: string): string {
342+
return JSON.stringify([project, workItemType]);
343+
}
344+
}

Diff for: src/plus/integrations/providers/azure/models.ts

+29
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
import type { IssueOrPullRequestState } from '../../../../git/models/issueOrPullRequest';
2+
3+
export type AzureWorkItemStateCategory = 'Proposed' | 'InProgress' | 'Resolved' | 'Completed' | 'Removed';
4+
5+
export function isClosedAzureWorkItemStateCategory(category: AzureWorkItemStateCategory | undefined): boolean {
6+
return category === 'Completed' || category === 'Resolved' || category === 'Removed';
7+
}
8+
9+
export function azureWorkItemsStateCategoryToState(
10+
category: AzureWorkItemStateCategory | undefined,
11+
): IssueOrPullRequestState {
12+
switch (category) {
13+
case 'Resolved':
14+
case 'Completed':
15+
case 'Removed':
16+
return 'closed';
17+
case 'Proposed':
18+
case 'InProgress':
19+
default:
20+
return 'opened';
21+
}
22+
}
23+
124
export interface AzureLink {
225
href: string;
326
}
@@ -46,3 +69,9 @@ export interface WorkItem {
4669
rev: number;
4770
url: string;
4871
}
72+
73+
export interface AzureWorkItemState {
74+
name: string;
75+
color: string;
76+
category: AzureWorkItemStateCategory;
77+
}

0 commit comments

Comments
 (0)