@elisra-devops/docgen-data-provider 1.105.0 → 1.107.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +28 -8
- package/README.md +7 -0
- package/bin/modules/TicketsDataProvider.d.ts +39 -1
- package/bin/modules/TicketsDataProvider.js +571 -23
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.historical.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.historical.test.js +478 -0
- package/bin/tests/modules/ticketsDataProvider.historical.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +162 -1
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +798 -95
- package/src/tests/modules/ticketsDataProvider.historical.test.ts +543 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +259 -44
|
@@ -27,6 +27,58 @@ type DocTypeBranchConfig = {
|
|
|
27
27
|
fallbackStart?: any;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
type HistoricalQueryListItem = {
|
|
31
|
+
id: string;
|
|
32
|
+
queryName: string;
|
|
33
|
+
path: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type HistoricalWorkItemSnapshot = {
|
|
37
|
+
id: number;
|
|
38
|
+
workItemType: string;
|
|
39
|
+
title: string;
|
|
40
|
+
state: string;
|
|
41
|
+
areaPath: string;
|
|
42
|
+
iterationPath: string;
|
|
43
|
+
versionId: number | null;
|
|
44
|
+
versionTimestamp: string;
|
|
45
|
+
description: string;
|
|
46
|
+
steps: string;
|
|
47
|
+
testPhase: string;
|
|
48
|
+
relatedLinkCount: number;
|
|
49
|
+
workItemUrl: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type HistoricalSnapshotResult = {
|
|
53
|
+
queryId: string;
|
|
54
|
+
queryName: string;
|
|
55
|
+
asOf: string;
|
|
56
|
+
total: number;
|
|
57
|
+
rows: HistoricalWorkItemSnapshot[];
|
|
58
|
+
snapshotMap: Map<number, HistoricalWorkItemSnapshot>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const HISTORICAL_WIT_API_VERSIONS: Array<string | null> = ['7.1', '5.1', null];
|
|
62
|
+
const HISTORICAL_BATCH_MAX_IDS = 200;
|
|
63
|
+
const HISTORICAL_WORK_ITEM_FIELDS = [
|
|
64
|
+
'System.Id',
|
|
65
|
+
'System.WorkItemType',
|
|
66
|
+
'System.Title',
|
|
67
|
+
'System.State',
|
|
68
|
+
'System.AreaPath',
|
|
69
|
+
'System.IterationPath',
|
|
70
|
+
'System.Rev',
|
|
71
|
+
'System.ChangedDate',
|
|
72
|
+
'System.Description',
|
|
73
|
+
'Microsoft.VSTS.TCM.Steps',
|
|
74
|
+
'Elisra.TestPhase',
|
|
75
|
+
'Custom.TestPhase',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Default fields fetched per work item in tree/flat query parsing. */
|
|
79
|
+
const WI_DEFAULT_FIELDS =
|
|
80
|
+
'System.Description,System.Title,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
|
|
81
|
+
|
|
30
82
|
export default class TicketsDataProvider {
|
|
31
83
|
orgUrl: string = '';
|
|
32
84
|
token: string = '';
|
|
@@ -187,12 +239,21 @@ export default class TicketsDataProvider {
|
|
|
187
239
|
* @param docType document type
|
|
188
240
|
* @returns
|
|
189
241
|
*/
|
|
242
|
+
private normalizeSharedQueriesPath(path: string): string {
|
|
243
|
+
const raw = String(path || '').trim();
|
|
244
|
+
if (!raw) return '';
|
|
245
|
+
const normalized = raw.toLowerCase();
|
|
246
|
+
if (normalized === 'shared' || normalized === 'shared queries') return '';
|
|
247
|
+
return raw;
|
|
248
|
+
}
|
|
249
|
+
|
|
190
250
|
async GetSharedQueries(project: string, path: string, docType: string = ''): Promise<any> {
|
|
191
251
|
let url;
|
|
192
252
|
try {
|
|
193
|
-
|
|
253
|
+
const normalizedPath = this.normalizeSharedQueriesPath(path);
|
|
254
|
+
if (normalizedPath === '')
|
|
194
255
|
url = `${this.orgUrl}${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all`;
|
|
195
|
-
else url = `${this.orgUrl}${project}/_apis/wit/queries/${
|
|
256
|
+
else url = `${this.orgUrl}${project}/_apis/wit/queries/${normalizedPath}?$depth=2&$expand=all`;
|
|
196
257
|
let queries: any = await TFSServices.getItemContent(url, this.token);
|
|
197
258
|
logger.debug(`doctype: ${docType}`);
|
|
198
259
|
const normalizedDocType = (docType || '').toLowerCase();
|
|
@@ -201,8 +262,7 @@ export default class TicketsDataProvider {
|
|
|
201
262
|
switch (normalizedDocType) {
|
|
202
263
|
case 'std':
|
|
203
264
|
case 'stp': {
|
|
204
|
-
const rootCandidates =
|
|
205
|
-
normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
|
|
265
|
+
const rootCandidates = normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
|
|
206
266
|
let stdRoot = queriesWithChildren;
|
|
207
267
|
let stdRootFound = false;
|
|
208
268
|
for (const candidate of rootCandidates) {
|
|
@@ -216,7 +276,7 @@ export default class TicketsDataProvider {
|
|
|
216
276
|
logger.debug(
|
|
217
277
|
`[GetSharedQueries][${normalizedDocType}] using ${
|
|
218
278
|
stdRootFound ? 'dedicated folder' : 'root queries'
|
|
219
|
-
}
|
|
279
|
+
}`,
|
|
220
280
|
);
|
|
221
281
|
// Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
|
|
222
282
|
const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
|
|
@@ -271,7 +331,7 @@ export default class TicketsDataProvider {
|
|
|
271
331
|
case 'str': {
|
|
272
332
|
const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(
|
|
273
333
|
queriesWithChildren,
|
|
274
|
-
'str'
|
|
334
|
+
'str',
|
|
275
335
|
);
|
|
276
336
|
logger.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
277
337
|
const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
|
|
@@ -338,12 +398,12 @@ export default class TicketsDataProvider {
|
|
|
338
398
|
case 'test-reporter': {
|
|
339
399
|
const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(
|
|
340
400
|
queriesWithChildren,
|
|
341
|
-
'test-reporter'
|
|
401
|
+
'test-reporter',
|
|
342
402
|
);
|
|
343
403
|
logger.debug(
|
|
344
404
|
`[GetSharedQueries][test-reporter] using ${
|
|
345
405
|
testReporterFound ? 'dedicated folder' : 'root queries'
|
|
346
|
-
}
|
|
406
|
+
}`,
|
|
347
407
|
);
|
|
348
408
|
const testReporterBranches = await this.fetchDocTypeBranches(
|
|
349
409
|
queriesWithChildren,
|
|
@@ -356,13 +416,15 @@ export default class TicketsDataProvider {
|
|
|
356
416
|
fetcher: (folder: any) => this.fetchTestReporterQueries(folder),
|
|
357
417
|
validator: (result: any) => this.hasAnyQueryTree(result?.testAssociatedTree),
|
|
358
418
|
},
|
|
359
|
-
]
|
|
419
|
+
],
|
|
360
420
|
);
|
|
361
421
|
const testReporterFetch = testReporterBranches['testReporter'];
|
|
362
422
|
return testReporterFetch?.result ?? { testAssociatedTree: null };
|
|
363
423
|
}
|
|
364
424
|
case 'srs':
|
|
365
425
|
return await this.fetchSrsQueries(queriesWithChildren);
|
|
426
|
+
case 'sysrs':
|
|
427
|
+
return await this.fetchSysRsQueries(queriesWithChildren);
|
|
366
428
|
case 'svd': {
|
|
367
429
|
const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
|
|
368
430
|
if (!found) {
|
|
@@ -399,6 +461,13 @@ export default class TicketsDataProvider {
|
|
|
399
461
|
knownBugsQueryTree: knownBugsFetch?.result ?? null,
|
|
400
462
|
};
|
|
401
463
|
}
|
|
464
|
+
case 'historical-query':
|
|
465
|
+
case 'historical': {
|
|
466
|
+
const { tree1 } = await this.structureAllQueryPath(queriesWithChildren);
|
|
467
|
+
return {
|
|
468
|
+
historicalQueryTree: tree1 ? [tree1] : [],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
402
471
|
default:
|
|
403
472
|
break;
|
|
404
473
|
}
|
|
@@ -429,7 +498,7 @@ export default class TicketsDataProvider {
|
|
|
429
498
|
field.name !== 'Title' &&
|
|
430
499
|
field.name !== 'Description' &&
|
|
431
500
|
field.name !== 'Work Item Type' &&
|
|
432
|
-
field.name !== 'Steps'
|
|
501
|
+
field.name !== 'Steps',
|
|
433
502
|
)
|
|
434
503
|
.map((field: any) => {
|
|
435
504
|
return {
|
|
@@ -456,7 +525,7 @@ export default class TicketsDataProvider {
|
|
|
456
525
|
onlyTestReq,
|
|
457
526
|
null,
|
|
458
527
|
['Requirement'],
|
|
459
|
-
['Test Case']
|
|
528
|
+
['Test Case'],
|
|
460
529
|
);
|
|
461
530
|
return { reqTestTree, testReqTree };
|
|
462
531
|
}
|
|
@@ -489,7 +558,7 @@ export default class TicketsDataProvider {
|
|
|
489
558
|
'Review',
|
|
490
559
|
'Test Plan',
|
|
491
560
|
'Test Suite',
|
|
492
|
-
]
|
|
561
|
+
],
|
|
493
562
|
);
|
|
494
563
|
return { linkedMomTree };
|
|
495
564
|
}
|
|
@@ -545,7 +614,7 @@ export default class TicketsDataProvider {
|
|
|
545
614
|
onlySourceSide,
|
|
546
615
|
null,
|
|
547
616
|
['Bug', 'Change Request'],
|
|
548
|
-
['Test Case']
|
|
617
|
+
['Test Case'],
|
|
549
618
|
);
|
|
550
619
|
return { OpenPcrToTestTree, TestToOpenPcrTree };
|
|
551
620
|
}
|
|
@@ -561,7 +630,7 @@ export default class TicketsDataProvider {
|
|
|
561
630
|
true,
|
|
562
631
|
null,
|
|
563
632
|
['Requirement', 'Bug', 'Change Request'],
|
|
564
|
-
['Test Case']
|
|
633
|
+
['Test Case'],
|
|
565
634
|
);
|
|
566
635
|
return { testAssociatedTree };
|
|
567
636
|
}
|
|
@@ -591,7 +660,7 @@ export default class TicketsDataProvider {
|
|
|
591
660
|
undefined,
|
|
592
661
|
true, // Enable processing of both tree and direct link queries, including flat queries
|
|
593
662
|
excludedFolderNames,
|
|
594
|
-
true
|
|
663
|
+
true,
|
|
595
664
|
);
|
|
596
665
|
return { systemRequirementsQueryTree };
|
|
597
666
|
}
|
|
@@ -617,19 +686,17 @@ export default class TicketsDataProvider {
|
|
|
617
686
|
|
|
618
687
|
const systemToSoftwareFolder = await this.findChildFolderByName(
|
|
619
688
|
srsFolderWithChildren,
|
|
620
|
-
'System to Software'
|
|
689
|
+
'System to Software',
|
|
621
690
|
);
|
|
622
691
|
const softwareToSystemFolder = await this.findChildFolderByName(
|
|
623
692
|
srsFolderWithChildren,
|
|
624
|
-
'Software to System'
|
|
693
|
+
'Software to System',
|
|
625
694
|
);
|
|
626
695
|
|
|
627
|
-
const systemToSoftwareRequirementsQueries =
|
|
628
|
-
systemToSoftwareFolder
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
softwareToSystemFolder
|
|
632
|
-
);
|
|
696
|
+
const systemToSoftwareRequirementsQueries =
|
|
697
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToSoftwareFolder);
|
|
698
|
+
const softwareToSystemRequirementsQueries =
|
|
699
|
+
await this.fetchRequirementsTraceQueriesForFolder(softwareToSystemFolder);
|
|
633
700
|
|
|
634
701
|
return {
|
|
635
702
|
systemRequirementsQueries,
|
|
@@ -638,6 +705,40 @@ export default class TicketsDataProvider {
|
|
|
638
705
|
};
|
|
639
706
|
}
|
|
640
707
|
|
|
708
|
+
private async fetchSysRsQueries(rootQueries: any) {
|
|
709
|
+
const { root: sysRsRoot, found: sysRsRootFound } = await this.getDocTypeRoot(rootQueries, 'sysrs');
|
|
710
|
+
logger.debug(`[GetSharedQueries][sysrs] using ${sysRsRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
711
|
+
|
|
712
|
+
const systemRequirementsQueries = await this.fetchSystemRequirementQueries(sysRsRoot, [
|
|
713
|
+
'System To Customer',
|
|
714
|
+
'System To Subsystem',
|
|
715
|
+
]);
|
|
716
|
+
|
|
717
|
+
const systemToCustomerFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
|
|
718
|
+
'system to customer',
|
|
719
|
+
'system-to-customer',
|
|
720
|
+
'system customer',
|
|
721
|
+
'subsystem to system',
|
|
722
|
+
'customer to system',
|
|
723
|
+
]);
|
|
724
|
+
const systemToSubsystemFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
|
|
725
|
+
'system to subsystem',
|
|
726
|
+
'system-to-subsystem',
|
|
727
|
+
'system subsystem',
|
|
728
|
+
]);
|
|
729
|
+
|
|
730
|
+
const subsystemToSystemRequirementsQueries =
|
|
731
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToCustomerFolder);
|
|
732
|
+
const systemToSubsystemRequirementsQueries =
|
|
733
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToSubsystemFolder);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
systemRequirementsQueries,
|
|
737
|
+
subsystemToSystemRequirementsQueries,
|
|
738
|
+
systemToSubsystemRequirementsQueries,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
641
742
|
/**
|
|
642
743
|
* Fetches and structures linked queries related to requirements traceability with area path filtering.
|
|
643
744
|
*
|
|
@@ -662,7 +763,7 @@ export default class TicketsDataProvider {
|
|
|
662
763
|
['epic', 'feature', 'requirement'],
|
|
663
764
|
['epic', 'feature', 'requirement'],
|
|
664
765
|
'sys', // Source area filter for tree1: System area paths
|
|
665
|
-
'soft' // Target area filter for tree1: Software area paths (tree2 will be reversed automatically)
|
|
766
|
+
'soft', // Target area filter for tree1: Software area paths (tree2 will be reversed automatically)
|
|
666
767
|
);
|
|
667
768
|
return { SystemToSoftwareRequirementsTree, SoftwareToSystemRequirementsTree };
|
|
668
769
|
}
|
|
@@ -680,7 +781,7 @@ export default class TicketsDataProvider {
|
|
|
680
781
|
['epic', 'feature', 'requirement'],
|
|
681
782
|
undefined,
|
|
682
783
|
undefined,
|
|
683
|
-
true
|
|
784
|
+
true,
|
|
684
785
|
);
|
|
685
786
|
return tree1;
|
|
686
787
|
}
|
|
@@ -727,7 +828,7 @@ export default class TicketsDataProvider {
|
|
|
727
828
|
const normalizedName = childName.toLowerCase();
|
|
728
829
|
return (
|
|
729
830
|
parentWithChildren.children.find(
|
|
730
|
-
(child: any) => child.isFolder && (child.name || '').toLowerCase() === normalizedName
|
|
831
|
+
(child: any) => child.isFolder && (child.name || '').toLowerCase() === normalizedName,
|
|
731
832
|
) || null
|
|
732
833
|
);
|
|
733
834
|
}
|
|
@@ -836,7 +937,7 @@ export default class TicketsDataProvider {
|
|
|
836
937
|
startingFolder: any,
|
|
837
938
|
fetcher: (folder: any) => Promise<any>,
|
|
838
939
|
logContext: string,
|
|
839
|
-
validator?: (result: any) => boolean
|
|
940
|
+
validator?: (result: any) => boolean,
|
|
840
941
|
): Promise<{ result: any; usedFolder: any }> {
|
|
841
942
|
const rootWithChildren = await this.ensureQueryChildren(rootQueries);
|
|
842
943
|
const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
|
|
@@ -870,7 +971,7 @@ export default class TicketsDataProvider {
|
|
|
870
971
|
private async fetchDocTypeBranches(
|
|
871
972
|
queriesWithChildren: any,
|
|
872
973
|
docRoot: any,
|
|
873
|
-
branches: DocTypeBranchConfig[]
|
|
974
|
+
branches: DocTypeBranchConfig[],
|
|
874
975
|
): Promise<Record<string, FallbackFetchOutcome>> {
|
|
875
976
|
const results: Record<string, FallbackFetchOutcome> = {};
|
|
876
977
|
const effectiveDocRoot = docRoot ?? queriesWithChildren;
|
|
@@ -884,7 +985,7 @@ export default class TicketsDataProvider {
|
|
|
884
985
|
if (branch.folderNames?.length && effectiveDocRoot) {
|
|
885
986
|
const resolvedFolder = await this.findChildFolderByPossibleNames(
|
|
886
987
|
effectiveDocRoot,
|
|
887
|
-
branch.folderNames
|
|
988
|
+
branch.folderNames,
|
|
888
989
|
);
|
|
889
990
|
if (resolvedFolder) {
|
|
890
991
|
startingFolder = resolvedFolder;
|
|
@@ -899,7 +1000,7 @@ export default class TicketsDataProvider {
|
|
|
899
1000
|
startingFolder,
|
|
900
1001
|
branch.fetcher,
|
|
901
1002
|
branch.label,
|
|
902
|
-
branch.validator
|
|
1003
|
+
branch.validator,
|
|
903
1004
|
);
|
|
904
1005
|
|
|
905
1006
|
logger.debug(`${branch.label} final folder: ${fetchOutcome.usedFolder?.name ?? '<root>'}`);
|
|
@@ -955,7 +1056,7 @@ export default class TicketsDataProvider {
|
|
|
955
1056
|
private async findPathToNode(
|
|
956
1057
|
currentNode: any,
|
|
957
1058
|
targetId: string,
|
|
958
|
-
visited: Set<string> = new Set<string>()
|
|
1059
|
+
visited: Set<string> = new Set<string>(),
|
|
959
1060
|
): Promise<any[] | null> {
|
|
960
1061
|
if (!currentNode) {
|
|
961
1062
|
return null;
|
|
@@ -989,7 +1090,7 @@ export default class TicketsDataProvider {
|
|
|
989
1090
|
|
|
990
1091
|
private async getDocTypeRoot(
|
|
991
1092
|
rootQueries: any,
|
|
992
|
-
docTypeName: string
|
|
1093
|
+
docTypeName: string,
|
|
993
1094
|
): Promise<{ root: any; found: boolean }> {
|
|
994
1095
|
if (!rootQueries) {
|
|
995
1096
|
return { root: rootQueries, found: false };
|
|
@@ -1022,7 +1123,8 @@ export default class TicketsDataProvider {
|
|
|
1022
1123
|
async GetQueryResultsFromWiql(
|
|
1023
1124
|
wiqlHref: string = '',
|
|
1024
1125
|
displayAsTable: boolean = false,
|
|
1025
|
-
testCaseToRelatedWiMap: Map<number, Set<any
|
|
1126
|
+
testCaseToRelatedWiMap: Map<number, Set<any>>,
|
|
1127
|
+
fetchAllFields: boolean = false,
|
|
1026
1128
|
): Promise<any> {
|
|
1027
1129
|
try {
|
|
1028
1130
|
if (!wiqlHref) {
|
|
@@ -1038,13 +1140,13 @@ export default class TicketsDataProvider {
|
|
|
1038
1140
|
case QueryType.OneHop:
|
|
1039
1141
|
return displayAsTable
|
|
1040
1142
|
? await this.parseDirectLinkedQueryResultForTableFormat(queryResult, testCaseToRelatedWiMap)
|
|
1041
|
-
: await this.parseTreeQueryResult(queryResult);
|
|
1143
|
+
: await this.parseTreeQueryResult(queryResult, fetchAllFields);
|
|
1042
1144
|
case QueryType.Tree:
|
|
1043
|
-
return await this.parseTreeQueryResult(queryResult);
|
|
1145
|
+
return await this.parseTreeQueryResult(queryResult, fetchAllFields);
|
|
1044
1146
|
case QueryType.Flat:
|
|
1045
1147
|
return displayAsTable
|
|
1046
1148
|
? await this.parseFlatQueryResultForTableFormat(queryResult)
|
|
1047
|
-
: await this.parseFlatQueryResult(queryResult);
|
|
1149
|
+
: await this.parseFlatQueryResult(queryResult, fetchAllFields);
|
|
1048
1150
|
default:
|
|
1049
1151
|
break;
|
|
1050
1152
|
}
|
|
@@ -1055,7 +1157,7 @@ export default class TicketsDataProvider {
|
|
|
1055
1157
|
|
|
1056
1158
|
private async parseDirectLinkedQueryResultForTableFormat(
|
|
1057
1159
|
queryResult: QueryTree,
|
|
1058
|
-
testCaseToRelatedWiMap: Map<number, Set<any
|
|
1160
|
+
testCaseToRelatedWiMap: Map<number, Set<any>>,
|
|
1059
1161
|
) {
|
|
1060
1162
|
const { columns, workItemRelations } = queryResult;
|
|
1061
1163
|
|
|
@@ -1104,17 +1206,17 @@ export default class TicketsDataProvider {
|
|
|
1104
1206
|
const allSourcePromises = Array.from(sourceIds).map((id) =>
|
|
1105
1207
|
this.limit(() => {
|
|
1106
1208
|
const relation = workItemRelations.find(
|
|
1107
|
-
(r) => (!r.source && r.target.id === id) || r.source?.id === id
|
|
1209
|
+
(r) => (!r.source && r.target.id === id) || r.source?.id === id,
|
|
1108
1210
|
);
|
|
1109
1211
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
|
|
1110
|
-
})
|
|
1212
|
+
}),
|
|
1111
1213
|
);
|
|
1112
1214
|
|
|
1113
1215
|
const allTargetPromises = Array.from(targetIds).map((id) =>
|
|
1114
1216
|
this.limit(() => {
|
|
1115
1217
|
const relation = workItemRelations.find((r) => r.target?.id === id);
|
|
1116
1218
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
|
|
1117
|
-
})
|
|
1219
|
+
}),
|
|
1118
1220
|
);
|
|
1119
1221
|
|
|
1120
1222
|
// Wait for all fetches to complete in parallel (with concurrency control)
|
|
@@ -1188,7 +1290,7 @@ export default class TicketsDataProvider {
|
|
|
1188
1290
|
private mapTestCaseToRelatedItem(
|
|
1189
1291
|
sourceWi: any,
|
|
1190
1292
|
targetWi: any,
|
|
1191
|
-
testCaseToRelatedItemMap: Map<number, Set<any
|
|
1293
|
+
testCaseToRelatedItemMap: Map<number, Set<any>>,
|
|
1192
1294
|
) {
|
|
1193
1295
|
if (sourceWi.fields['System.WorkItemType'] == 'Test Case') {
|
|
1194
1296
|
if (!testCaseToRelatedItemMap.has(sourceWi.id)) {
|
|
@@ -1230,7 +1332,7 @@ export default class TicketsDataProvider {
|
|
|
1230
1332
|
const wiSet: Set<any> = new Set();
|
|
1231
1333
|
if (workItems) {
|
|
1232
1334
|
const fetchPromises = workItems.map((workItem) =>
|
|
1233
|
-
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false))
|
|
1335
|
+
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)),
|
|
1234
1336
|
);
|
|
1235
1337
|
|
|
1236
1338
|
const fetchedWorkItems = await Promise.all(fetchPromises);
|
|
@@ -1244,7 +1346,7 @@ export default class TicketsDataProvider {
|
|
|
1244
1346
|
};
|
|
1245
1347
|
}
|
|
1246
1348
|
|
|
1247
|
-
private async parseTreeQueryResult(queryResult: QueryTree) {
|
|
1349
|
+
private async parseTreeQueryResult(queryResult: QueryTree, fetchAllFields: boolean = false) {
|
|
1248
1350
|
const { workItemRelations } = queryResult;
|
|
1249
1351
|
if (!workItemRelations) return null;
|
|
1250
1352
|
|
|
@@ -1258,11 +1360,11 @@ export default class TicketsDataProvider {
|
|
|
1258
1360
|
// This ensures nodes with non-hierarchy links are also available
|
|
1259
1361
|
for (const rel of workItemRelations) {
|
|
1260
1362
|
const t = rel.target;
|
|
1261
|
-
if (!allItems[t.id]) await this.initTreeQueryResultItem(t, allItems);
|
|
1363
|
+
if (!allItems[t.id]) await this.initTreeQueryResultItem(t, allItems, fetchAllFields);
|
|
1262
1364
|
|
|
1263
1365
|
// Also initialize source nodes if they exist
|
|
1264
1366
|
if (rel.source && !allItems[rel.source.id]) {
|
|
1265
|
-
await this.initTreeQueryResultItem(rel.source, allItems);
|
|
1367
|
+
await this.initTreeQueryResultItem(rel.source, allItems, fetchAllFields);
|
|
1266
1368
|
}
|
|
1267
1369
|
|
|
1268
1370
|
if (rel.rel === null && rel.source === null) {
|
|
@@ -1273,7 +1375,7 @@ export default class TicketsDataProvider {
|
|
|
1273
1375
|
}
|
|
1274
1376
|
}
|
|
1275
1377
|
logger.debug(
|
|
1276
|
-
`parseTreeQueryResult: Found ${rootOrder.length} roots, ${Object.keys(allItems).length} total nodes
|
|
1378
|
+
`parseTreeQueryResult: Found ${rootOrder.length} roots, ${Object.keys(allItems).length} total nodes`,
|
|
1277
1379
|
);
|
|
1278
1380
|
|
|
1279
1381
|
// Attach only forward hierarchy edges; dedupe children by id per parent
|
|
@@ -1293,11 +1395,11 @@ export default class TicketsDataProvider {
|
|
|
1293
1395
|
// Nodes should already be initialized, but double-check
|
|
1294
1396
|
if (!allItems[parentId]) {
|
|
1295
1397
|
logger.warn(`Parent ${parentId} not found, initializing now`);
|
|
1296
|
-
await this.initTreeQueryResultItem(rel.source, allItems);
|
|
1398
|
+
await this.initTreeQueryResultItem(rel.source, allItems, fetchAllFields);
|
|
1297
1399
|
}
|
|
1298
1400
|
if (!allItems[childId]) {
|
|
1299
1401
|
logger.warn(`Child ${childId} not found, initializing now`);
|
|
1300
|
-
await this.initTreeQueryResultItem(rel.target, allItems);
|
|
1402
|
+
await this.initTreeQueryResultItem(rel.target, allItems, fetchAllFields);
|
|
1301
1403
|
}
|
|
1302
1404
|
|
|
1303
1405
|
const parent = allItems[parentId];
|
|
@@ -1313,13 +1415,13 @@ export default class TicketsDataProvider {
|
|
|
1313
1415
|
}
|
|
1314
1416
|
}
|
|
1315
1417
|
logger.debug(
|
|
1316
|
-
`parseTreeQueryResult: ${hierarchyCount} hierarchy links, ${skippedNonHierarchy} non-hierarchy links skipped
|
|
1418
|
+
`parseTreeQueryResult: ${hierarchyCount} hierarchy links, ${skippedNonHierarchy} non-hierarchy links skipped`,
|
|
1317
1419
|
);
|
|
1318
1420
|
|
|
1319
1421
|
// Return roots in original order, excluding those that became children
|
|
1320
1422
|
const roots = rootOrder.filter((id) => rootSet.has(id)).map((id) => allItems[id]);
|
|
1321
1423
|
logger.debug(
|
|
1322
|
-
`parseTreeQueryResult: Returning ${roots.length} roots with ${Object.keys(allItems).length} total items
|
|
1424
|
+
`parseTreeQueryResult: Returning ${roots.length} roots with ${Object.keys(allItems).length} total items`,
|
|
1323
1425
|
);
|
|
1324
1426
|
|
|
1325
1427
|
// Optional: clean helper sets
|
|
@@ -1332,32 +1434,42 @@ export default class TicketsDataProvider {
|
|
|
1332
1434
|
};
|
|
1333
1435
|
}
|
|
1334
1436
|
|
|
1335
|
-
private async initTreeQueryResultItem(item: any, allItems: any) {
|
|
1336
|
-
const urlWi = `${item.url}?fields
|
|
1437
|
+
private async initTreeQueryResultItem(item: any, allItems: any, fetchAllFields: boolean = false) {
|
|
1438
|
+
const urlWi = fetchAllFields ? `${item.url}` : `${item.url}?fields=${WI_DEFAULT_FIELDS}`;
|
|
1337
1439
|
const wi = await TFSServices.getItemContent(urlWi, this.token);
|
|
1440
|
+
const fields = wi?.fields || {};
|
|
1338
1441
|
// need to fetch the WI with only the the title, the web URL and the description
|
|
1339
1442
|
allItems[item.id] = {
|
|
1340
1443
|
id: item.id,
|
|
1341
|
-
title:
|
|
1342
|
-
description:
|
|
1444
|
+
title: fields['System.Title'] || '',
|
|
1445
|
+
description: fields['Microsoft.VSTS.CMMI.Symptom'] ?? fields['System.Description'] ?? '',
|
|
1343
1446
|
htmlUrl: wi._links.html.href,
|
|
1447
|
+
fields,
|
|
1448
|
+
workItemType: fields['System.WorkItemType'] || '',
|
|
1344
1449
|
children: [],
|
|
1345
1450
|
};
|
|
1346
1451
|
}
|
|
1347
1452
|
|
|
1348
|
-
private async initFlatQueryResultItem(
|
|
1349
|
-
|
|
1453
|
+
private async initFlatQueryResultItem(
|
|
1454
|
+
item: any,
|
|
1455
|
+
workItemMap: Map<number, any>,
|
|
1456
|
+
fetchAllFields: boolean = false,
|
|
1457
|
+
) {
|
|
1458
|
+
const urlWi = fetchAllFields ? `${item.url}` : `${item.url}?fields=${WI_DEFAULT_FIELDS}`;
|
|
1350
1459
|
const wi = await TFSServices.getItemContent(urlWi, this.token);
|
|
1460
|
+
const fields = wi?.fields || {};
|
|
1351
1461
|
// need to fetch the WI with only the the title, the web URL and the description
|
|
1352
1462
|
workItemMap.set(item.id, {
|
|
1353
1463
|
id: item.id,
|
|
1354
|
-
title:
|
|
1355
|
-
description:
|
|
1464
|
+
title: fields['System.Title'] || '',
|
|
1465
|
+
description: fields['Microsoft.VSTS.CMMI.Symptom'] ?? fields['System.Description'] ?? '',
|
|
1356
1466
|
htmlUrl: wi._links.html.href,
|
|
1467
|
+
fields,
|
|
1468
|
+
workItemType: fields['System.WorkItemType'] || '',
|
|
1357
1469
|
});
|
|
1358
1470
|
}
|
|
1359
1471
|
|
|
1360
|
-
private async parseFlatQueryResult(queryResult: QueryTree) {
|
|
1472
|
+
private async parseFlatQueryResult(queryResult: QueryTree, fetchAllFields: boolean = false) {
|
|
1361
1473
|
const { workItems } = queryResult;
|
|
1362
1474
|
if (!workItems) {
|
|
1363
1475
|
logger.warn(`No work items were found for this requested query`);
|
|
@@ -1368,7 +1480,7 @@ export default class TicketsDataProvider {
|
|
|
1368
1480
|
const workItemsResultMap: Map<number, any> = new Map();
|
|
1369
1481
|
for (const wi of workItems) {
|
|
1370
1482
|
if (!workItemsResultMap.has(wi.id)) {
|
|
1371
|
-
await this.initFlatQueryResultItem(wi, workItemsResultMap);
|
|
1483
|
+
await this.initFlatQueryResultItem(wi, workItemsResultMap, fetchAllFields);
|
|
1372
1484
|
}
|
|
1373
1485
|
}
|
|
1374
1486
|
return [...workItemsResultMap.values()];
|
|
@@ -1381,7 +1493,7 @@ export default class TicketsDataProvider {
|
|
|
1381
1493
|
receivedObject: any,
|
|
1382
1494
|
columnMap: Map<string, string>,
|
|
1383
1495
|
resultedRefNameMap: Map<string, string>,
|
|
1384
|
-
isRelation: boolean
|
|
1496
|
+
isRelation: boolean,
|
|
1385
1497
|
) {
|
|
1386
1498
|
const url = isRelation ? `${receivedObject.target.url}` : `${receivedObject.url}`;
|
|
1387
1499
|
const wi: any = await TFSServices.getItemContent(url, this.token);
|
|
@@ -1414,7 +1526,7 @@ export default class TicketsDataProvider {
|
|
|
1414
1526
|
if (modeledResult.queryType == 'tree') {
|
|
1415
1527
|
let levelResults: Array<Workitem> = Helper.LevelBuilder(
|
|
1416
1528
|
modeledResult,
|
|
1417
|
-
modeledResult.workItems[0].fields[0].value
|
|
1529
|
+
modeledResult.workItems[0].fields[0].value,
|
|
1418
1530
|
);
|
|
1419
1531
|
return levelResults;
|
|
1420
1532
|
}
|
|
@@ -1442,6 +1554,597 @@ export default class TicketsDataProvider {
|
|
|
1442
1554
|
return await this.GetQueryResultsByWiqlHref(wiql.href, project);
|
|
1443
1555
|
}
|
|
1444
1556
|
|
|
1557
|
+
private normalizeHistoricalAsOf(value: string): string {
|
|
1558
|
+
const raw = String(value || '').trim();
|
|
1559
|
+
if (!raw) {
|
|
1560
|
+
throw new Error('asOf date-time is required');
|
|
1561
|
+
}
|
|
1562
|
+
const parsed = new Date(raw);
|
|
1563
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1564
|
+
throw new Error(`Invalid date-time value: ${raw}`);
|
|
1565
|
+
}
|
|
1566
|
+
return parsed.toISOString();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
private normalizeHistoricalCompareValue(value: unknown): string {
|
|
1570
|
+
if (value == null) {
|
|
1571
|
+
return '';
|
|
1572
|
+
}
|
|
1573
|
+
if (typeof value === 'string') {
|
|
1574
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
1575
|
+
}
|
|
1576
|
+
if (Array.isArray(value)) {
|
|
1577
|
+
return value
|
|
1578
|
+
.map((item) => this.normalizeHistoricalCompareValue(item))
|
|
1579
|
+
.filter((item) => item !== '')
|
|
1580
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1581
|
+
.join('; ');
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof value === 'object') {
|
|
1584
|
+
const fallback = value as Record<string, unknown>;
|
|
1585
|
+
if (typeof fallback.displayName === 'string') {
|
|
1586
|
+
return this.normalizeHistoricalCompareValue(fallback.displayName);
|
|
1587
|
+
}
|
|
1588
|
+
if (typeof fallback.name === 'string') {
|
|
1589
|
+
return this.normalizeHistoricalCompareValue(fallback.name);
|
|
1590
|
+
}
|
|
1591
|
+
return this.normalizeHistoricalCompareValue(JSON.stringify(value));
|
|
1592
|
+
}
|
|
1593
|
+
return String(value).trim();
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
private normalizeTestPhaseValue(value: unknown): string {
|
|
1597
|
+
const rendered = this.normalizeHistoricalCompareValue(value);
|
|
1598
|
+
if (!rendered) {
|
|
1599
|
+
return '';
|
|
1600
|
+
}
|
|
1601
|
+
const parts = rendered
|
|
1602
|
+
.split(/[;,]/)
|
|
1603
|
+
.map((part) => part.trim())
|
|
1604
|
+
.filter((part) => part.length > 0);
|
|
1605
|
+
if (parts.length <= 1) {
|
|
1606
|
+
return rendered;
|
|
1607
|
+
}
|
|
1608
|
+
const unique = Array.from(new Set(parts));
|
|
1609
|
+
unique.sort((a, b) => a.localeCompare(b));
|
|
1610
|
+
return unique.join('; ');
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private isTestCaseType(workItemType: string): boolean {
|
|
1614
|
+
const normalized = String(workItemType || '').trim().toLowerCase();
|
|
1615
|
+
return normalized === 'test case' || normalized === 'testcase';
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
private toHistoricalRevision(value: unknown): number | null {
|
|
1619
|
+
const n = Number(value);
|
|
1620
|
+
if (!Number.isFinite(n)) {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
return n;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
private extractHistoricalWorkItemIds(queryResult: any): number[] {
|
|
1627
|
+
const ids = new Set<number>();
|
|
1628
|
+
|
|
1629
|
+
const pushId = (candidate: unknown) => {
|
|
1630
|
+
const n = Number(candidate);
|
|
1631
|
+
if (Number.isFinite(n)) {
|
|
1632
|
+
ids.add(n);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
const workItems = Array.isArray(queryResult?.workItems) ? queryResult.workItems : [];
|
|
1637
|
+
for (const workItem of workItems) {
|
|
1638
|
+
pushId(workItem?.id);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const workItemRelations = Array.isArray(queryResult?.workItemRelations)
|
|
1642
|
+
? queryResult.workItemRelations
|
|
1643
|
+
: [];
|
|
1644
|
+
for (const relation of workItemRelations) {
|
|
1645
|
+
pushId(relation?.source?.id);
|
|
1646
|
+
pushId(relation?.target?.id);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
return Array.from(ids.values()).sort((a, b) => a - b);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
private appendAsOfToWiql(wiql: string, asOfIso: string): string {
|
|
1653
|
+
const raw = String(wiql || '').trim();
|
|
1654
|
+
if (!raw) {
|
|
1655
|
+
return '';
|
|
1656
|
+
}
|
|
1657
|
+
const withoutAsOf = raw.replace(/\s+ASOF\s+'[^']*'/i, '');
|
|
1658
|
+
return `${withoutAsOf} ASOF '${asOfIso}'`;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
private appendApiVersion(url: string, apiVersion: string | null): string {
|
|
1662
|
+
if (!apiVersion) {
|
|
1663
|
+
return url;
|
|
1664
|
+
}
|
|
1665
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
1666
|
+
return `${url}${separator}api-version=${encodeURIComponent(apiVersion)}`;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
private shouldRetryHistoricalWithLowerVersion(error: any): boolean {
|
|
1670
|
+
const status = Number(error?.response?.status || 0);
|
|
1671
|
+
if (status === 401 || status === 403) {
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
if (status >= 500) {
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
if ([400, 404, 405, 406, 410].includes(status)) {
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
const message = String(error?.response?.data?.message || error?.message || '');
|
|
1681
|
+
return /api[- ]?version/i.test(message);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
private async withHistoricalApiVersionFallback<T>(
|
|
1685
|
+
operation: string,
|
|
1686
|
+
task: (apiVersion: string | null) => Promise<T>,
|
|
1687
|
+
): Promise<{ apiVersion: string | null; result: T }> {
|
|
1688
|
+
let lastError: any = null;
|
|
1689
|
+
for (const apiVersion of HISTORICAL_WIT_API_VERSIONS) {
|
|
1690
|
+
try {
|
|
1691
|
+
const result = await task(apiVersion);
|
|
1692
|
+
return { apiVersion, result };
|
|
1693
|
+
} catch (error: any) {
|
|
1694
|
+
lastError = error;
|
|
1695
|
+
const status = Number(error?.response?.status || 0);
|
|
1696
|
+
const apiVersionLabel = apiVersion || 'default';
|
|
1697
|
+
logger.warn(
|
|
1698
|
+
`[${operation}] failed with api-version=${apiVersionLabel}${status ? ` (status ${status})` : ''}`,
|
|
1699
|
+
);
|
|
1700
|
+
if (
|
|
1701
|
+
!this.shouldRetryHistoricalWithLowerVersion(error) ||
|
|
1702
|
+
apiVersion === HISTORICAL_WIT_API_VERSIONS[HISTORICAL_WIT_API_VERSIONS.length - 1]
|
|
1703
|
+
) {
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
throw lastError || new Error(`[${operation}] Failed to execute historical request`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
private chunkHistoricalWorkItemIds(ids: number[]): number[][] {
|
|
1712
|
+
const chunks: number[][] = [];
|
|
1713
|
+
for (let i = 0; i < ids.length; i += HISTORICAL_BATCH_MAX_IDS) {
|
|
1714
|
+
chunks.push(ids.slice(i, i + HISTORICAL_BATCH_MAX_IDS));
|
|
1715
|
+
}
|
|
1716
|
+
return chunks;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
private normalizeHistoricalQueryPath(path: string): string {
|
|
1720
|
+
const rawPath = String(path || '').trim();
|
|
1721
|
+
const normalizedRoot = rawPath.toLowerCase();
|
|
1722
|
+
if (
|
|
1723
|
+
rawPath === '' ||
|
|
1724
|
+
normalizedRoot === 'shared' ||
|
|
1725
|
+
normalizedRoot === 'shared queries'
|
|
1726
|
+
) {
|
|
1727
|
+
return 'Shared%20Queries';
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const segments = rawPath
|
|
1731
|
+
.replace(/\\/g, '/')
|
|
1732
|
+
.split('/')
|
|
1733
|
+
.filter((segment) => segment.trim() !== '')
|
|
1734
|
+
.map((segment) => {
|
|
1735
|
+
try {
|
|
1736
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
return encodeURIComponent(segment);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
return segments.join('/');
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
private normalizeHistoricalQueryRoot(root: any): any {
|
|
1745
|
+
if (!root) return root;
|
|
1746
|
+
if (Array.isArray(root?.children) || typeof root?.isFolder === 'boolean') {
|
|
1747
|
+
return root;
|
|
1748
|
+
}
|
|
1749
|
+
if (Array.isArray(root?.value)) {
|
|
1750
|
+
return {
|
|
1751
|
+
id: 'root',
|
|
1752
|
+
name: 'Shared Queries',
|
|
1753
|
+
isFolder: true,
|
|
1754
|
+
children: root.value,
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
return root;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
private async fetchHistoricalWorkItemsBatch(
|
|
1761
|
+
project: string,
|
|
1762
|
+
ids: number[],
|
|
1763
|
+
asOf: string,
|
|
1764
|
+
apiVersion: string | null,
|
|
1765
|
+
): Promise<any[]> {
|
|
1766
|
+
const workItemsBatchUrl = this.appendApiVersion(
|
|
1767
|
+
`${this.orgUrl}${project}/_apis/wit/workitemsbatch`,
|
|
1768
|
+
apiVersion,
|
|
1769
|
+
);
|
|
1770
|
+
const idChunks = this.chunkHistoricalWorkItemIds(ids);
|
|
1771
|
+
try {
|
|
1772
|
+
const batchResponses = await Promise.all(
|
|
1773
|
+
idChunks.map((idChunk) =>
|
|
1774
|
+
this.limit(() =>
|
|
1775
|
+
TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1776
|
+
ids: idChunk,
|
|
1777
|
+
asOf,
|
|
1778
|
+
$expand: 'Relations',
|
|
1779
|
+
fields: HISTORICAL_WORK_ITEM_FIELDS,
|
|
1780
|
+
}),
|
|
1781
|
+
),
|
|
1782
|
+
),
|
|
1783
|
+
);
|
|
1784
|
+
return batchResponses.flatMap((batch) => (Array.isArray(batch?.value) ? batch.value : []));
|
|
1785
|
+
} catch (error: any) {
|
|
1786
|
+
logger.warn(
|
|
1787
|
+
`[historical-workitems] workitemsbatch failed${
|
|
1788
|
+
apiVersion ? ` (api-version=${apiVersion})` : ''
|
|
1789
|
+
}, falling back to per-item retrieval: ${error?.message || error}`,
|
|
1790
|
+
);
|
|
1791
|
+
const asOfParam = encodeURIComponent(asOf);
|
|
1792
|
+
const responses = await Promise.all(
|
|
1793
|
+
ids.map((id) =>
|
|
1794
|
+
this.limit(() => {
|
|
1795
|
+
const itemUrl = this.appendApiVersion(
|
|
1796
|
+
`${this.orgUrl}${project}/_apis/wit/workitems/${id}?$expand=Relations&asOf=${asOfParam}`,
|
|
1797
|
+
apiVersion,
|
|
1798
|
+
);
|
|
1799
|
+
return TFSServices.getItemContent(itemUrl, this.token);
|
|
1800
|
+
}),
|
|
1801
|
+
),
|
|
1802
|
+
);
|
|
1803
|
+
return responses.filter((item) => item && typeof item === 'object');
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
private async executeHistoricalQueryAtAsOf(
|
|
1808
|
+
project: string,
|
|
1809
|
+
queryId: string,
|
|
1810
|
+
asOfIso: string,
|
|
1811
|
+
): Promise<{ queryDefinition: any; queryResult: any; apiVersion: string | null }> {
|
|
1812
|
+
const { apiVersion, result } = await this.withHistoricalApiVersionFallback(
|
|
1813
|
+
'historical-query-execution',
|
|
1814
|
+
async (resolvedApiVersion) => {
|
|
1815
|
+
const queryDefUrl = this.appendApiVersion(
|
|
1816
|
+
`${this.orgUrl}${project}/_apis/wit/queries/${encodeURIComponent(queryId)}?$expand=all`,
|
|
1817
|
+
resolvedApiVersion,
|
|
1818
|
+
);
|
|
1819
|
+
const queryDefinition = await TFSServices.getItemContent(queryDefUrl, this.token);
|
|
1820
|
+
|
|
1821
|
+
const wiqlText = String(queryDefinition?.wiql || '').trim();
|
|
1822
|
+
let queryResult: any = null;
|
|
1823
|
+
try {
|
|
1824
|
+
if (!wiqlText) {
|
|
1825
|
+
throw new Error(`Could not resolve WIQL text for query ${queryId}`);
|
|
1826
|
+
}
|
|
1827
|
+
const wiqlWithAsOf = this.appendAsOfToWiql(wiqlText, asOfIso);
|
|
1828
|
+
if (!wiqlWithAsOf) {
|
|
1829
|
+
throw new Error(`Could not build WIQL for historical query ${queryId}`);
|
|
1830
|
+
}
|
|
1831
|
+
const executeUrl = this.appendApiVersion(
|
|
1832
|
+
`${this.orgUrl}${project}/_apis/wit/wiql?$top=2147483646&timePrecision=true`,
|
|
1833
|
+
resolvedApiVersion,
|
|
1834
|
+
);
|
|
1835
|
+
queryResult = await TFSServices.getItemContent(executeUrl, this.token, 'post', {
|
|
1836
|
+
query: wiqlWithAsOf,
|
|
1837
|
+
});
|
|
1838
|
+
} catch (inlineWiqlError: any) {
|
|
1839
|
+
logger.warn(
|
|
1840
|
+
`[historical-query-execution] inline WIQL failed for query ${queryId}${
|
|
1841
|
+
resolvedApiVersion ? ` (api-version=${resolvedApiVersion})` : ''
|
|
1842
|
+
}, trying WIQL-by-id fallback: ${inlineWiqlError?.message || inlineWiqlError}`,
|
|
1843
|
+
);
|
|
1844
|
+
const executeByIdUrl = this.appendApiVersion(
|
|
1845
|
+
`${this.orgUrl}${project}/_apis/wit/wiql/${encodeURIComponent(
|
|
1846
|
+
queryId,
|
|
1847
|
+
)}?$top=2147483646&timePrecision=true&asOf=${encodeURIComponent(asOfIso)}`,
|
|
1848
|
+
resolvedApiVersion,
|
|
1849
|
+
);
|
|
1850
|
+
queryResult = await TFSServices.getItemContent(executeByIdUrl, this.token);
|
|
1851
|
+
}
|
|
1852
|
+
return { queryDefinition, queryResult };
|
|
1853
|
+
},
|
|
1854
|
+
);
|
|
1855
|
+
return { ...result, apiVersion };
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
private toHistoricalWorkItemSnapshot(project: string, workItem: any): HistoricalWorkItemSnapshot {
|
|
1859
|
+
const fields = (workItem?.fields || {}) as Record<string, unknown>;
|
|
1860
|
+
const id = Number(workItem?.id || fields['System.Id'] || 0);
|
|
1861
|
+
const workItemType = this.normalizeHistoricalCompareValue(fields['System.WorkItemType']);
|
|
1862
|
+
const testPhaseRaw =
|
|
1863
|
+
fields['Elisra.TestPhase'] ??
|
|
1864
|
+
fields['Custom.TestPhase'] ??
|
|
1865
|
+
fields['Elisra.Testphase'] ??
|
|
1866
|
+
fields['Custom.Testphase'];
|
|
1867
|
+
const relatedLinkCount = Array.isArray(workItem?.relations) ? workItem.relations.length : 0;
|
|
1868
|
+
|
|
1869
|
+
return {
|
|
1870
|
+
id,
|
|
1871
|
+
workItemType,
|
|
1872
|
+
title: this.normalizeHistoricalCompareValue(fields['System.Title']),
|
|
1873
|
+
state: this.normalizeHistoricalCompareValue(fields['System.State']),
|
|
1874
|
+
areaPath: this.normalizeHistoricalCompareValue(fields['System.AreaPath']),
|
|
1875
|
+
iterationPath: this.normalizeHistoricalCompareValue(fields['System.IterationPath']),
|
|
1876
|
+
versionId: this.toHistoricalRevision(workItem?.rev ?? fields['System.Rev']),
|
|
1877
|
+
versionTimestamp: this.normalizeHistoricalCompareValue(fields['System.ChangedDate']),
|
|
1878
|
+
description: this.normalizeHistoricalCompareValue(fields['System.Description']),
|
|
1879
|
+
steps: this.normalizeHistoricalCompareValue(fields['Microsoft.VSTS.TCM.Steps']),
|
|
1880
|
+
testPhase: this.normalizeTestPhaseValue(testPhaseRaw),
|
|
1881
|
+
relatedLinkCount,
|
|
1882
|
+
workItemUrl: `${this.orgUrl}${project}/_workitems/edit/${id}`,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
private async getHistoricalSnapshot(
|
|
1887
|
+
project: string,
|
|
1888
|
+
queryId: string,
|
|
1889
|
+
asOfInput: string,
|
|
1890
|
+
): Promise<HistoricalSnapshotResult> {
|
|
1891
|
+
const asOf = this.normalizeHistoricalAsOf(asOfInput);
|
|
1892
|
+
const { queryDefinition, queryResult, apiVersion } = await this.executeHistoricalQueryAtAsOf(
|
|
1893
|
+
project,
|
|
1894
|
+
queryId,
|
|
1895
|
+
asOf,
|
|
1896
|
+
);
|
|
1897
|
+
const ids = this.extractHistoricalWorkItemIds(queryResult);
|
|
1898
|
+
|
|
1899
|
+
if (ids.length === 0) {
|
|
1900
|
+
return {
|
|
1901
|
+
queryId,
|
|
1902
|
+
queryName: String(queryDefinition?.name || queryId),
|
|
1903
|
+
asOf,
|
|
1904
|
+
total: 0,
|
|
1905
|
+
rows: [],
|
|
1906
|
+
snapshotMap: new Map<number, HistoricalWorkItemSnapshot>(),
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const values = await this.fetchHistoricalWorkItemsBatch(project, ids, asOf, apiVersion);
|
|
1911
|
+
const rows = values
|
|
1912
|
+
.map((workItem: any) => this.toHistoricalWorkItemSnapshot(project, workItem))
|
|
1913
|
+
.sort((a: HistoricalWorkItemSnapshot, b: HistoricalWorkItemSnapshot) => a.id - b.id);
|
|
1914
|
+
const snapshotMap = new Map<number, HistoricalWorkItemSnapshot>();
|
|
1915
|
+
for (const row of rows) {
|
|
1916
|
+
snapshotMap.set(row.id, row);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
return {
|
|
1920
|
+
queryId,
|
|
1921
|
+
queryName: String(queryDefinition?.name || queryId),
|
|
1922
|
+
asOf,
|
|
1923
|
+
total: rows.length,
|
|
1924
|
+
rows,
|
|
1925
|
+
snapshotMap,
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private collectHistoricalQueries(root: any, parentPath = ''): HistoricalQueryListItem[] {
|
|
1930
|
+
const items: HistoricalQueryListItem[] = [];
|
|
1931
|
+
if (!root) {
|
|
1932
|
+
return items;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
const name = String(root?.name || '').trim();
|
|
1936
|
+
const currentPath = parentPath && name ? `${parentPath}/${name}` : name || parentPath;
|
|
1937
|
+
|
|
1938
|
+
if (!root?.isFolder) {
|
|
1939
|
+
const id = String(root?.id || '').trim();
|
|
1940
|
+
if (id) {
|
|
1941
|
+
items.push({
|
|
1942
|
+
id,
|
|
1943
|
+
queryName: name || id,
|
|
1944
|
+
path: parentPath || 'Shared Queries',
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
return items;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const children = Array.isArray(root?.children) ? root.children : [];
|
|
1951
|
+
for (const child of children) {
|
|
1952
|
+
items.push(...this.collectHistoricalQueries(child, currentPath || 'Shared Queries'));
|
|
1953
|
+
}
|
|
1954
|
+
return items;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Returns a flat list of shared queries for historical/as-of execution.
|
|
1959
|
+
*/
|
|
1960
|
+
async GetHistoricalQueries(project: string, path: string = 'shared'): Promise<HistoricalQueryListItem[]> {
|
|
1961
|
+
const normalizedPath = this.normalizeHistoricalQueryPath(path);
|
|
1962
|
+
// Azure DevOps WIT query tree endpoint enforces $depth range 0..2.
|
|
1963
|
+
const depth = 2;
|
|
1964
|
+
const { result: root } = await this.withHistoricalApiVersionFallback('historical-queries-list', (apiVersion) => {
|
|
1965
|
+
const url = this.appendApiVersion(
|
|
1966
|
+
`${this.orgUrl}${project}/_apis/wit/queries/${normalizedPath}?$depth=${depth}&$expand=all`,
|
|
1967
|
+
apiVersion,
|
|
1968
|
+
);
|
|
1969
|
+
return TFSServices.getItemContent(url, this.token);
|
|
1970
|
+
});
|
|
1971
|
+
const normalizedRoot = this.normalizeHistoricalQueryRoot(root);
|
|
1972
|
+
const items = this.collectHistoricalQueries(normalizedRoot).filter((query) => query.id !== '');
|
|
1973
|
+
items.sort((a, b) => {
|
|
1974
|
+
const byPath = a.path.localeCompare(b.path);
|
|
1975
|
+
if (byPath !== 0) return byPath;
|
|
1976
|
+
return a.queryName.localeCompare(b.queryName);
|
|
1977
|
+
});
|
|
1978
|
+
return items;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Runs a shared query as-of a specific date-time and returns a flat work-item table snapshot.
|
|
1983
|
+
*/
|
|
1984
|
+
async GetHistoricalQueryResults(queryId: string, project: string, asOfInput: string): Promise<any> {
|
|
1985
|
+
const snapshot = await this.getHistoricalSnapshot(project, queryId, asOfInput);
|
|
1986
|
+
return {
|
|
1987
|
+
queryId: snapshot.queryId,
|
|
1988
|
+
queryName: snapshot.queryName,
|
|
1989
|
+
asOf: snapshot.asOf,
|
|
1990
|
+
total: snapshot.total,
|
|
1991
|
+
rows: snapshot.rows.map((row) => ({
|
|
1992
|
+
id: row.id,
|
|
1993
|
+
workItemType: row.workItemType,
|
|
1994
|
+
title: row.title,
|
|
1995
|
+
state: row.state,
|
|
1996
|
+
areaPath: row.areaPath,
|
|
1997
|
+
iterationPath: row.iterationPath,
|
|
1998
|
+
versionId: row.versionId,
|
|
1999
|
+
versionTimestamp: row.versionTimestamp,
|
|
2000
|
+
workItemUrl: row.workItemUrl,
|
|
2001
|
+
})),
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Compares a shared query between two date-time baselines using the feature's noise-control fields.
|
|
2007
|
+
*/
|
|
2008
|
+
async CompareHistoricalQueryResults(
|
|
2009
|
+
queryId: string,
|
|
2010
|
+
project: string,
|
|
2011
|
+
baselineAsOfInput: string,
|
|
2012
|
+
compareToAsOfInput: string,
|
|
2013
|
+
): Promise<any> {
|
|
2014
|
+
const [baseline, compareTo] = await Promise.all([
|
|
2015
|
+
this.getHistoricalSnapshot(project, queryId, baselineAsOfInput),
|
|
2016
|
+
this.getHistoricalSnapshot(project, queryId, compareToAsOfInput),
|
|
2017
|
+
]);
|
|
2018
|
+
|
|
2019
|
+
const allIds = new Set<number>([
|
|
2020
|
+
...Array.from(baseline.snapshotMap.keys()),
|
|
2021
|
+
...Array.from(compareTo.snapshotMap.keys()),
|
|
2022
|
+
]);
|
|
2023
|
+
const sortedIds = Array.from(allIds.values()).sort((a, b) => a - b);
|
|
2024
|
+
|
|
2025
|
+
const rows = sortedIds.map((id) => {
|
|
2026
|
+
const baselineRow = baseline.snapshotMap.get(id) || null;
|
|
2027
|
+
const compareToRow = compareTo.snapshotMap.get(id) || null;
|
|
2028
|
+
const workItemType = compareToRow?.workItemType || baselineRow?.workItemType || '';
|
|
2029
|
+
const isTestCase = this.isTestCaseType(workItemType);
|
|
2030
|
+
|
|
2031
|
+
if (baselineRow && !compareToRow) {
|
|
2032
|
+
return {
|
|
2033
|
+
id,
|
|
2034
|
+
workItemType,
|
|
2035
|
+
title: baselineRow.title,
|
|
2036
|
+
baselineRevisionId: baselineRow.versionId,
|
|
2037
|
+
compareToRevisionId: null,
|
|
2038
|
+
compareStatus: 'Deleted',
|
|
2039
|
+
changedFields: [],
|
|
2040
|
+
differences: [],
|
|
2041
|
+
workItemUrl: baselineRow.workItemUrl,
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (!baselineRow && compareToRow) {
|
|
2046
|
+
return {
|
|
2047
|
+
id,
|
|
2048
|
+
workItemType,
|
|
2049
|
+
title: compareToRow.title,
|
|
2050
|
+
baselineRevisionId: null,
|
|
2051
|
+
compareToRevisionId: compareToRow.versionId,
|
|
2052
|
+
compareStatus: 'Added',
|
|
2053
|
+
changedFields: [],
|
|
2054
|
+
differences: [],
|
|
2055
|
+
workItemUrl: compareToRow.workItemUrl,
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
const safeBaseline = baselineRow as HistoricalWorkItemSnapshot;
|
|
2060
|
+
const safeCompareTo = compareToRow as HistoricalWorkItemSnapshot;
|
|
2061
|
+
const changedFields: string[] = [];
|
|
2062
|
+
|
|
2063
|
+
if (safeBaseline.description !== safeCompareTo.description) {
|
|
2064
|
+
changedFields.push('Description');
|
|
2065
|
+
}
|
|
2066
|
+
if (safeBaseline.title !== safeCompareTo.title) {
|
|
2067
|
+
changedFields.push('Title');
|
|
2068
|
+
}
|
|
2069
|
+
if (safeBaseline.state !== safeCompareTo.state) {
|
|
2070
|
+
changedFields.push('State');
|
|
2071
|
+
}
|
|
2072
|
+
if (isTestCase && safeBaseline.steps !== safeCompareTo.steps) {
|
|
2073
|
+
changedFields.push('Steps');
|
|
2074
|
+
}
|
|
2075
|
+
if (safeBaseline.testPhase !== safeCompareTo.testPhase) {
|
|
2076
|
+
changedFields.push('Test Phase');
|
|
2077
|
+
}
|
|
2078
|
+
if (isTestCase && safeBaseline.relatedLinkCount !== safeCompareTo.relatedLinkCount) {
|
|
2079
|
+
changedFields.push('Related Link Count');
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const differences = changedFields.map((field) => {
|
|
2083
|
+
switch (field) {
|
|
2084
|
+
case 'Description':
|
|
2085
|
+
return { field, baseline: safeBaseline.description, compareTo: safeCompareTo.description };
|
|
2086
|
+
case 'Title':
|
|
2087
|
+
return { field, baseline: safeBaseline.title, compareTo: safeCompareTo.title };
|
|
2088
|
+
case 'State':
|
|
2089
|
+
return { field, baseline: safeBaseline.state, compareTo: safeCompareTo.state };
|
|
2090
|
+
case 'Steps':
|
|
2091
|
+
return { field, baseline: safeBaseline.steps, compareTo: safeCompareTo.steps };
|
|
2092
|
+
case 'Test Phase':
|
|
2093
|
+
return { field, baseline: safeBaseline.testPhase, compareTo: safeCompareTo.testPhase };
|
|
2094
|
+
case 'Related Link Count':
|
|
2095
|
+
return {
|
|
2096
|
+
field,
|
|
2097
|
+
baseline: String(safeBaseline.relatedLinkCount),
|
|
2098
|
+
compareTo: String(safeCompareTo.relatedLinkCount),
|
|
2099
|
+
};
|
|
2100
|
+
default:
|
|
2101
|
+
return { field, baseline: '', compareTo: '' };
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
return {
|
|
2106
|
+
id,
|
|
2107
|
+
workItemType,
|
|
2108
|
+
title: safeCompareTo.title || safeBaseline.title,
|
|
2109
|
+
baselineRevisionId: safeBaseline.versionId,
|
|
2110
|
+
compareToRevisionId: safeCompareTo.versionId,
|
|
2111
|
+
compareStatus: changedFields.length > 0 ? 'Changed' : 'No changes',
|
|
2112
|
+
changedFields,
|
|
2113
|
+
differences,
|
|
2114
|
+
workItemUrl: safeCompareTo.workItemUrl || safeBaseline.workItemUrl,
|
|
2115
|
+
};
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
const summary = rows.reduce(
|
|
2119
|
+
(acc, row) => {
|
|
2120
|
+
if (row.compareStatus === 'Added') acc.addedCount += 1;
|
|
2121
|
+
else if (row.compareStatus === 'Deleted') acc.deletedCount += 1;
|
|
2122
|
+
else if (row.compareStatus === 'Changed') acc.changedCount += 1;
|
|
2123
|
+
else acc.noChangeCount += 1;
|
|
2124
|
+
return acc;
|
|
2125
|
+
},
|
|
2126
|
+
{ addedCount: 0, deletedCount: 0, changedCount: 0, noChangeCount: 0 },
|
|
2127
|
+
);
|
|
2128
|
+
|
|
2129
|
+
return {
|
|
2130
|
+
queryId,
|
|
2131
|
+
queryName: compareTo.queryName || baseline.queryName || queryId,
|
|
2132
|
+
baseline: {
|
|
2133
|
+
asOf: baseline.asOf,
|
|
2134
|
+
total: baseline.total,
|
|
2135
|
+
},
|
|
2136
|
+
compareTo: {
|
|
2137
|
+
asOf: compareTo.asOf,
|
|
2138
|
+
total: compareTo.total,
|
|
2139
|
+
},
|
|
2140
|
+
summary: {
|
|
2141
|
+
...summary,
|
|
2142
|
+
updatedCount: summary.changedCount,
|
|
2143
|
+
},
|
|
2144
|
+
rows,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1445
2148
|
async PopulateWorkItemsByIds(workItemsArray: any[] = [], projectName: string = ''): Promise<any[]> {
|
|
1446
2149
|
let url = `${this.orgUrl}${projectName}/_apis/wit/workitemsbatch`;
|
|
1447
2150
|
let res: any[] = [];
|
|
@@ -1585,7 +2288,7 @@ export default class TicketsDataProvider {
|
|
|
1585
2288
|
|
|
1586
2289
|
async CreateNewWorkItem(projectName: string, wiBody: any, wiType: string, byPass: boolean) {
|
|
1587
2290
|
let url = `${this.orgUrl}${projectName}/_apis/wit/workitems/$${wiType}?bypassRules=${String(
|
|
1588
|
-
byPass
|
|
2291
|
+
byPass,
|
|
1589
2292
|
).toString()}`;
|
|
1590
2293
|
return TFSServices.getItemContent(url, this.token, 'POST', wiBody, {
|
|
1591
2294
|
'Content-Type': 'application/json-patch+json',
|
|
@@ -1605,7 +2308,7 @@ export default class TicketsDataProvider {
|
|
|
1605
2308
|
attachment.downloadUrl = `${relation.url}/${relation.attributes.name}`;
|
|
1606
2309
|
attachmentList.push(attachment);
|
|
1607
2310
|
}
|
|
1608
|
-
})
|
|
2311
|
+
}),
|
|
1609
2312
|
);
|
|
1610
2313
|
return attachmentList;
|
|
1611
2314
|
} catch (e) {
|
|
@@ -1626,7 +2329,7 @@ export default class TicketsDataProvider {
|
|
|
1626
2329
|
async UpdateWorkItem(projectName: string, wiBody: any, workItemId: number, byPass: boolean) {
|
|
1627
2330
|
let res: any;
|
|
1628
2331
|
let url: string = `${this.orgUrl}${projectName}/_apis/wit/workitems/${workItemId}?bypassRules=${String(
|
|
1629
|
-
byPass
|
|
2332
|
+
byPass,
|
|
1630
2333
|
).toString()}`;
|
|
1631
2334
|
res = await TFSServices.getItemContent(url, this.token, 'patch', wiBody, {
|
|
1632
2335
|
'Content-Type': 'application/json-patch+json',
|
|
@@ -1680,7 +2383,7 @@ export default class TicketsDataProvider {
|
|
|
1680
2383
|
|
|
1681
2384
|
// Process children recursively
|
|
1682
2385
|
const childResults = await Promise.all(
|
|
1683
|
-
rootQuery.children.map((child: any) => this.structureAllQueryPath(child, rootQuery.id))
|
|
2386
|
+
rootQuery.children.map((child: any) => this.structureAllQueryPath(child, rootQuery.id)),
|
|
1684
2387
|
);
|
|
1685
2388
|
|
|
1686
2389
|
// Build tree
|
|
@@ -1718,8 +2421,8 @@ export default class TicketsDataProvider {
|
|
|
1718
2421
|
} catch (err: any) {
|
|
1719
2422
|
logger.error(
|
|
1720
2423
|
`Error occurred while constructing the query list ${err.message} with query ${JSON.stringify(
|
|
1721
|
-
rootQuery
|
|
1722
|
-
)}
|
|
2424
|
+
rootQuery,
|
|
2425
|
+
)}`,
|
|
1723
2426
|
);
|
|
1724
2427
|
throw err;
|
|
1725
2428
|
}
|
|
@@ -1757,7 +2460,7 @@ export default class TicketsDataProvider {
|
|
|
1757
2460
|
includeTreeQueries: boolean = false,
|
|
1758
2461
|
excludedFolderNames: string[] = [],
|
|
1759
2462
|
includeFlatQueries: boolean = false,
|
|
1760
|
-
workItemTypeCache?: Map<string, string | null
|
|
2463
|
+
workItemTypeCache?: Map<string, string | null>,
|
|
1761
2464
|
): Promise<any> {
|
|
1762
2465
|
try {
|
|
1763
2466
|
// Per-invocation cache for ID->WorkItemType lookups; avoids global state and is safe for concurrency.
|
|
@@ -1765,7 +2468,7 @@ export default class TicketsDataProvider {
|
|
|
1765
2468
|
const shouldSkipFolder =
|
|
1766
2469
|
rootQuery?.isFolder &&
|
|
1767
2470
|
excludedFolderNames.some(
|
|
1768
|
-
(folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase()
|
|
2471
|
+
(folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase(),
|
|
1769
2472
|
);
|
|
1770
2473
|
|
|
1771
2474
|
if (shouldSkipFolder) {
|
|
@@ -1789,7 +2492,7 @@ export default class TicketsDataProvider {
|
|
|
1789
2492
|
rootQuery,
|
|
1790
2493
|
wiql,
|
|
1791
2494
|
allTypes,
|
|
1792
|
-
typeCache
|
|
2495
|
+
typeCache,
|
|
1793
2496
|
);
|
|
1794
2497
|
|
|
1795
2498
|
if (typesOk) {
|
|
@@ -1816,7 +2519,7 @@ export default class TicketsDataProvider {
|
|
|
1816
2519
|
wiql,
|
|
1817
2520
|
sources,
|
|
1818
2521
|
targets,
|
|
1819
|
-
typeCache
|
|
2522
|
+
typeCache,
|
|
1820
2523
|
);
|
|
1821
2524
|
}
|
|
1822
2525
|
const matchesReverse = await this.matchesSourceTargetConditionAsync(
|
|
@@ -1824,7 +2527,7 @@ export default class TicketsDataProvider {
|
|
|
1824
2527
|
wiql,
|
|
1825
2528
|
targets,
|
|
1826
2529
|
sources,
|
|
1827
|
-
typeCache
|
|
2530
|
+
typeCache,
|
|
1828
2531
|
);
|
|
1829
2532
|
|
|
1830
2533
|
if (matchesForward) {
|
|
@@ -1871,7 +2574,7 @@ export default class TicketsDataProvider {
|
|
|
1871
2574
|
includeTreeQueries,
|
|
1872
2575
|
excludedFolderNames,
|
|
1873
2576
|
includeFlatQueries,
|
|
1874
|
-
typeCache
|
|
2577
|
+
typeCache,
|
|
1875
2578
|
);
|
|
1876
2579
|
}
|
|
1877
2580
|
|
|
@@ -1889,9 +2592,9 @@ export default class TicketsDataProvider {
|
|
|
1889
2592
|
includeTreeQueries,
|
|
1890
2593
|
excludedFolderNames,
|
|
1891
2594
|
includeFlatQueries,
|
|
1892
|
-
typeCache
|
|
1893
|
-
)
|
|
1894
|
-
)
|
|
2595
|
+
typeCache,
|
|
2596
|
+
),
|
|
2597
|
+
),
|
|
1895
2598
|
);
|
|
1896
2599
|
|
|
1897
2600
|
// Build tree1
|
|
@@ -1924,8 +2627,8 @@ export default class TicketsDataProvider {
|
|
|
1924
2627
|
} catch (err: any) {
|
|
1925
2628
|
logger.error(
|
|
1926
2629
|
`Error occurred while constructing the query list ${err.message} with query ${JSON.stringify(
|
|
1927
|
-
rootQuery
|
|
1928
|
-
)}
|
|
2630
|
+
rootQuery,
|
|
2631
|
+
)}`,
|
|
1929
2632
|
);
|
|
1930
2633
|
logger.error(`Error stack ${err.message}`);
|
|
1931
2634
|
}
|
|
@@ -1941,7 +2644,7 @@ export default class TicketsDataProvider {
|
|
|
1941
2644
|
private matchesAreaPathCondition(
|
|
1942
2645
|
wiql: string,
|
|
1943
2646
|
sourceAreaFilter: string,
|
|
1944
|
-
targetAreaFilter: string
|
|
2647
|
+
targetAreaFilter: string,
|
|
1945
2648
|
): boolean {
|
|
1946
2649
|
const wiqlLower = (wiql || '').toLowerCase();
|
|
1947
2650
|
const srcFilter = (sourceAreaFilter || '').toLowerCase().trim();
|
|
@@ -1979,7 +2682,7 @@ export default class TicketsDataProvider {
|
|
|
1979
2682
|
wiql: string,
|
|
1980
2683
|
source: string[],
|
|
1981
2684
|
target: string[],
|
|
1982
|
-
workItemTypeCache: Map<string, string | null
|
|
2685
|
+
workItemTypeCache: Map<string, string | null>,
|
|
1983
2686
|
): Promise<boolean> {
|
|
1984
2687
|
/**
|
|
1985
2688
|
* Matches source+target constraints for link WIQL.
|
|
@@ -1992,7 +2695,7 @@ export default class TicketsDataProvider {
|
|
|
1992
2695
|
wiql,
|
|
1993
2696
|
'Source',
|
|
1994
2697
|
source,
|
|
1995
|
-
workItemTypeCache
|
|
2698
|
+
workItemTypeCache,
|
|
1996
2699
|
);
|
|
1997
2700
|
if (!sourceOk) return false;
|
|
1998
2701
|
const targetOk = await this.isLinkSideAllowedByTypeOrId(
|
|
@@ -2000,7 +2703,7 @@ export default class TicketsDataProvider {
|
|
|
2000
2703
|
wiql,
|
|
2001
2704
|
'Target',
|
|
2002
2705
|
target,
|
|
2003
|
-
workItemTypeCache
|
|
2706
|
+
workItemTypeCache,
|
|
2004
2707
|
);
|
|
2005
2708
|
return targetOk;
|
|
2006
2709
|
}
|
|
@@ -2009,7 +2712,7 @@ export default class TicketsDataProvider {
|
|
|
2009
2712
|
queryNode: any,
|
|
2010
2713
|
wiql: string,
|
|
2011
2714
|
allowedTypes: string[],
|
|
2012
|
-
workItemTypeCache: Map<string, string | null
|
|
2715
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2013
2716
|
): Promise<boolean> {
|
|
2014
2717
|
return this.isFlatQueryAllowedByTypeOrId(queryNode, wiql, allowedTypes, workItemTypeCache);
|
|
2015
2718
|
}
|
|
@@ -2149,7 +2852,7 @@ export default class TicketsDataProvider {
|
|
|
2149
2852
|
wiql: string,
|
|
2150
2853
|
context: 'Source' | 'Target',
|
|
2151
2854
|
allowedTypes: string[],
|
|
2152
|
-
workItemTypeCache: Map<string, string | null
|
|
2855
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2153
2856
|
): Promise<boolean> {
|
|
2154
2857
|
const wiqlStr = String(wiql || '');
|
|
2155
2858
|
|
|
@@ -2157,7 +2860,7 @@ export default class TicketsDataProvider {
|
|
|
2157
2860
|
if (!allowedTypes || allowedTypes.length === 0) {
|
|
2158
2861
|
const fieldPresenceRegex = new RegExp(
|
|
2159
2862
|
`${this.buildWiqlFieldPattern(context, 'System.WorkItemType')}`,
|
|
2160
|
-
'i'
|
|
2863
|
+
'i',
|
|
2161
2864
|
);
|
|
2162
2865
|
return fieldPresenceRegex.test(wiqlStr);
|
|
2163
2866
|
}
|
|
@@ -2197,7 +2900,7 @@ export default class TicketsDataProvider {
|
|
|
2197
2900
|
queryNode: any,
|
|
2198
2901
|
wiql: string,
|
|
2199
2902
|
allowedTypes: string[],
|
|
2200
|
-
workItemTypeCache: Map<string, string | null
|
|
2903
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2201
2904
|
): Promise<boolean> {
|
|
2202
2905
|
const wiqlStr = String(wiql || '');
|
|
2203
2906
|
|
|
@@ -2240,7 +2943,7 @@ export default class TicketsDataProvider {
|
|
|
2240
2943
|
private async getWorkItemTypeById(
|
|
2241
2944
|
project: string,
|
|
2242
2945
|
id: string,
|
|
2243
|
-
workItemTypeCache: Map<string, string | null
|
|
2946
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2244
2947
|
): Promise<string | null> {
|
|
2245
2948
|
const cacheKey = `${project}:${id}`;
|
|
2246
2949
|
if (workItemTypeCache.has(cacheKey)) {
|
|
@@ -2298,7 +3001,7 @@ export default class TicketsDataProvider {
|
|
|
2298
3001
|
private filterFieldsByColumns(
|
|
2299
3002
|
item: any,
|
|
2300
3003
|
columnsToFilterMap: Map<string, string>,
|
|
2301
|
-
resultedRefNameMap: Map<string, string
|
|
3004
|
+
resultedRefNameMap: Map<string, string>,
|
|
2302
3005
|
) {
|
|
2303
3006
|
try {
|
|
2304
3007
|
const parsedFields: any = {};
|
|
@@ -2345,14 +3048,14 @@ export default class TicketsDataProvider {
|
|
|
2345
3048
|
this.token,
|
|
2346
3049
|
'get',
|
|
2347
3050
|
{},
|
|
2348
|
-
{ Accept: accept }
|
|
3051
|
+
{ Accept: accept },
|
|
2349
3052
|
);
|
|
2350
3053
|
if (iconDataUrl) break;
|
|
2351
3054
|
} catch (error: any) {
|
|
2352
3055
|
logger.warn(
|
|
2353
3056
|
`Failed to download icon (${accept}) for work item type ${
|
|
2354
3057
|
workItemType?.name ?? 'unknown'
|
|
2355
|
-
}: ${error?.message || error}
|
|
3058
|
+
}: ${error?.message || error}`,
|
|
2356
3059
|
);
|
|
2357
3060
|
}
|
|
2358
3061
|
}
|
|
@@ -2361,8 +3064,8 @@ export default class TicketsDataProvider {
|
|
|
2361
3064
|
const iconPayload = workItemType.icon
|
|
2362
3065
|
? { ...workItemType.icon, dataUrl: iconDataUrl }
|
|
2363
3066
|
: iconDataUrl
|
|
2364
|
-
|
|
2365
|
-
|
|
3067
|
+
? { id: undefined, url: undefined, dataUrl: iconDataUrl }
|
|
3068
|
+
: workItemType.icon;
|
|
2366
3069
|
|
|
2367
3070
|
return {
|
|
2368
3071
|
name: workItemType.name,
|
|
@@ -2371,7 +3074,7 @@ export default class TicketsDataProvider {
|
|
|
2371
3074
|
icon: iconPayload,
|
|
2372
3075
|
states: workItemType.states,
|
|
2373
3076
|
};
|
|
2374
|
-
})
|
|
3077
|
+
}),
|
|
2375
3078
|
);
|
|
2376
3079
|
|
|
2377
3080
|
return workItemTypesWithIcons;
|
|
@@ -2543,7 +3246,7 @@ export default class TicketsDataProvider {
|
|
|
2543
3246
|
});
|
|
2544
3247
|
|
|
2545
3248
|
logger.debug(
|
|
2546
|
-
`Categorized ${workItemIds.length} work items into ${Object.keys(finalCategories).length} categories
|
|
3249
|
+
`Categorized ${workItemIds.length} work items into ${Object.keys(finalCategories).length} categories`,
|
|
2547
3250
|
);
|
|
2548
3251
|
|
|
2549
3252
|
return {
|