@elisra-devops/docgen-data-provider 1.105.0 → 1.106.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/bin/modules/TicketsDataProvider.d.ts +2 -1
- package/bin/modules/TicketsDataProvider.js +57 -21
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +128 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +141 -93
- package/src/tests/modules/ticketsDataProvider.test.ts +209 -43
|
@@ -27,6 +27,10 @@ type DocTypeBranchConfig = {
|
|
|
27
27
|
fallbackStart?: any;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/** Default fields fetched per work item in tree/flat query parsing. */
|
|
31
|
+
const WI_DEFAULT_FIELDS =
|
|
32
|
+
'System.Description,System.Title,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
|
|
33
|
+
|
|
30
34
|
export default class TicketsDataProvider {
|
|
31
35
|
orgUrl: string = '';
|
|
32
36
|
token: string = '';
|
|
@@ -201,8 +205,7 @@ export default class TicketsDataProvider {
|
|
|
201
205
|
switch (normalizedDocType) {
|
|
202
206
|
case 'std':
|
|
203
207
|
case 'stp': {
|
|
204
|
-
const rootCandidates =
|
|
205
|
-
normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
|
|
208
|
+
const rootCandidates = normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
|
|
206
209
|
let stdRoot = queriesWithChildren;
|
|
207
210
|
let stdRootFound = false;
|
|
208
211
|
for (const candidate of rootCandidates) {
|
|
@@ -216,7 +219,7 @@ export default class TicketsDataProvider {
|
|
|
216
219
|
logger.debug(
|
|
217
220
|
`[GetSharedQueries][${normalizedDocType}] using ${
|
|
218
221
|
stdRootFound ? 'dedicated folder' : 'root queries'
|
|
219
|
-
}
|
|
222
|
+
}`,
|
|
220
223
|
);
|
|
221
224
|
// Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
|
|
222
225
|
const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
|
|
@@ -271,7 +274,7 @@ export default class TicketsDataProvider {
|
|
|
271
274
|
case 'str': {
|
|
272
275
|
const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(
|
|
273
276
|
queriesWithChildren,
|
|
274
|
-
'str'
|
|
277
|
+
'str',
|
|
275
278
|
);
|
|
276
279
|
logger.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
277
280
|
const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
|
|
@@ -338,12 +341,12 @@ export default class TicketsDataProvider {
|
|
|
338
341
|
case 'test-reporter': {
|
|
339
342
|
const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(
|
|
340
343
|
queriesWithChildren,
|
|
341
|
-
'test-reporter'
|
|
344
|
+
'test-reporter',
|
|
342
345
|
);
|
|
343
346
|
logger.debug(
|
|
344
347
|
`[GetSharedQueries][test-reporter] using ${
|
|
345
348
|
testReporterFound ? 'dedicated folder' : 'root queries'
|
|
346
|
-
}
|
|
349
|
+
}`,
|
|
347
350
|
);
|
|
348
351
|
const testReporterBranches = await this.fetchDocTypeBranches(
|
|
349
352
|
queriesWithChildren,
|
|
@@ -356,13 +359,15 @@ export default class TicketsDataProvider {
|
|
|
356
359
|
fetcher: (folder: any) => this.fetchTestReporterQueries(folder),
|
|
357
360
|
validator: (result: any) => this.hasAnyQueryTree(result?.testAssociatedTree),
|
|
358
361
|
},
|
|
359
|
-
]
|
|
362
|
+
],
|
|
360
363
|
);
|
|
361
364
|
const testReporterFetch = testReporterBranches['testReporter'];
|
|
362
365
|
return testReporterFetch?.result ?? { testAssociatedTree: null };
|
|
363
366
|
}
|
|
364
367
|
case 'srs':
|
|
365
368
|
return await this.fetchSrsQueries(queriesWithChildren);
|
|
369
|
+
case 'sysrs':
|
|
370
|
+
return await this.fetchSysRsQueries(queriesWithChildren);
|
|
366
371
|
case 'svd': {
|
|
367
372
|
const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
|
|
368
373
|
if (!found) {
|
|
@@ -429,7 +434,7 @@ export default class TicketsDataProvider {
|
|
|
429
434
|
field.name !== 'Title' &&
|
|
430
435
|
field.name !== 'Description' &&
|
|
431
436
|
field.name !== 'Work Item Type' &&
|
|
432
|
-
field.name !== 'Steps'
|
|
437
|
+
field.name !== 'Steps',
|
|
433
438
|
)
|
|
434
439
|
.map((field: any) => {
|
|
435
440
|
return {
|
|
@@ -456,7 +461,7 @@ export default class TicketsDataProvider {
|
|
|
456
461
|
onlyTestReq,
|
|
457
462
|
null,
|
|
458
463
|
['Requirement'],
|
|
459
|
-
['Test Case']
|
|
464
|
+
['Test Case'],
|
|
460
465
|
);
|
|
461
466
|
return { reqTestTree, testReqTree };
|
|
462
467
|
}
|
|
@@ -489,7 +494,7 @@ export default class TicketsDataProvider {
|
|
|
489
494
|
'Review',
|
|
490
495
|
'Test Plan',
|
|
491
496
|
'Test Suite',
|
|
492
|
-
]
|
|
497
|
+
],
|
|
493
498
|
);
|
|
494
499
|
return { linkedMomTree };
|
|
495
500
|
}
|
|
@@ -545,7 +550,7 @@ export default class TicketsDataProvider {
|
|
|
545
550
|
onlySourceSide,
|
|
546
551
|
null,
|
|
547
552
|
['Bug', 'Change Request'],
|
|
548
|
-
['Test Case']
|
|
553
|
+
['Test Case'],
|
|
549
554
|
);
|
|
550
555
|
return { OpenPcrToTestTree, TestToOpenPcrTree };
|
|
551
556
|
}
|
|
@@ -561,7 +566,7 @@ export default class TicketsDataProvider {
|
|
|
561
566
|
true,
|
|
562
567
|
null,
|
|
563
568
|
['Requirement', 'Bug', 'Change Request'],
|
|
564
|
-
['Test Case']
|
|
569
|
+
['Test Case'],
|
|
565
570
|
);
|
|
566
571
|
return { testAssociatedTree };
|
|
567
572
|
}
|
|
@@ -591,7 +596,7 @@ export default class TicketsDataProvider {
|
|
|
591
596
|
undefined,
|
|
592
597
|
true, // Enable processing of both tree and direct link queries, including flat queries
|
|
593
598
|
excludedFolderNames,
|
|
594
|
-
true
|
|
599
|
+
true,
|
|
595
600
|
);
|
|
596
601
|
return { systemRequirementsQueryTree };
|
|
597
602
|
}
|
|
@@ -617,19 +622,17 @@ export default class TicketsDataProvider {
|
|
|
617
622
|
|
|
618
623
|
const systemToSoftwareFolder = await this.findChildFolderByName(
|
|
619
624
|
srsFolderWithChildren,
|
|
620
|
-
'System to Software'
|
|
625
|
+
'System to Software',
|
|
621
626
|
);
|
|
622
627
|
const softwareToSystemFolder = await this.findChildFolderByName(
|
|
623
628
|
srsFolderWithChildren,
|
|
624
|
-
'Software to System'
|
|
629
|
+
'Software to System',
|
|
625
630
|
);
|
|
626
631
|
|
|
627
|
-
const systemToSoftwareRequirementsQueries =
|
|
628
|
-
systemToSoftwareFolder
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
softwareToSystemFolder
|
|
632
|
-
);
|
|
632
|
+
const systemToSoftwareRequirementsQueries =
|
|
633
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToSoftwareFolder);
|
|
634
|
+
const softwareToSystemRequirementsQueries =
|
|
635
|
+
await this.fetchRequirementsTraceQueriesForFolder(softwareToSystemFolder);
|
|
633
636
|
|
|
634
637
|
return {
|
|
635
638
|
systemRequirementsQueries,
|
|
@@ -638,6 +641,40 @@ export default class TicketsDataProvider {
|
|
|
638
641
|
};
|
|
639
642
|
}
|
|
640
643
|
|
|
644
|
+
private async fetchSysRsQueries(rootQueries: any) {
|
|
645
|
+
const { root: sysRsRoot, found: sysRsRootFound } = await this.getDocTypeRoot(rootQueries, 'sysrs');
|
|
646
|
+
logger.debug(`[GetSharedQueries][sysrs] using ${sysRsRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
647
|
+
|
|
648
|
+
const systemRequirementsQueries = await this.fetchSystemRequirementQueries(sysRsRoot, [
|
|
649
|
+
'System To Customer',
|
|
650
|
+
'System To Subsystem',
|
|
651
|
+
]);
|
|
652
|
+
|
|
653
|
+
const systemToCustomerFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
|
|
654
|
+
'system to customer',
|
|
655
|
+
'system-to-customer',
|
|
656
|
+
'system customer',
|
|
657
|
+
'subsystem to system',
|
|
658
|
+
'customer to system',
|
|
659
|
+
]);
|
|
660
|
+
const systemToSubsystemFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
|
|
661
|
+
'system to subsystem',
|
|
662
|
+
'system-to-subsystem',
|
|
663
|
+
'system subsystem',
|
|
664
|
+
]);
|
|
665
|
+
|
|
666
|
+
const subsystemToSystemRequirementsQueries =
|
|
667
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToCustomerFolder);
|
|
668
|
+
const systemToSubsystemRequirementsQueries =
|
|
669
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToSubsystemFolder);
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
systemRequirementsQueries,
|
|
673
|
+
subsystemToSystemRequirementsQueries,
|
|
674
|
+
systemToSubsystemRequirementsQueries,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
641
678
|
/**
|
|
642
679
|
* Fetches and structures linked queries related to requirements traceability with area path filtering.
|
|
643
680
|
*
|
|
@@ -662,7 +699,7 @@ export default class TicketsDataProvider {
|
|
|
662
699
|
['epic', 'feature', 'requirement'],
|
|
663
700
|
['epic', 'feature', 'requirement'],
|
|
664
701
|
'sys', // Source area filter for tree1: System area paths
|
|
665
|
-
'soft' // Target area filter for tree1: Software area paths (tree2 will be reversed automatically)
|
|
702
|
+
'soft', // Target area filter for tree1: Software area paths (tree2 will be reversed automatically)
|
|
666
703
|
);
|
|
667
704
|
return { SystemToSoftwareRequirementsTree, SoftwareToSystemRequirementsTree };
|
|
668
705
|
}
|
|
@@ -680,7 +717,7 @@ export default class TicketsDataProvider {
|
|
|
680
717
|
['epic', 'feature', 'requirement'],
|
|
681
718
|
undefined,
|
|
682
719
|
undefined,
|
|
683
|
-
true
|
|
720
|
+
true,
|
|
684
721
|
);
|
|
685
722
|
return tree1;
|
|
686
723
|
}
|
|
@@ -727,7 +764,7 @@ export default class TicketsDataProvider {
|
|
|
727
764
|
const normalizedName = childName.toLowerCase();
|
|
728
765
|
return (
|
|
729
766
|
parentWithChildren.children.find(
|
|
730
|
-
(child: any) => child.isFolder && (child.name || '').toLowerCase() === normalizedName
|
|
767
|
+
(child: any) => child.isFolder && (child.name || '').toLowerCase() === normalizedName,
|
|
731
768
|
) || null
|
|
732
769
|
);
|
|
733
770
|
}
|
|
@@ -836,7 +873,7 @@ export default class TicketsDataProvider {
|
|
|
836
873
|
startingFolder: any,
|
|
837
874
|
fetcher: (folder: any) => Promise<any>,
|
|
838
875
|
logContext: string,
|
|
839
|
-
validator?: (result: any) => boolean
|
|
876
|
+
validator?: (result: any) => boolean,
|
|
840
877
|
): Promise<{ result: any; usedFolder: any }> {
|
|
841
878
|
const rootWithChildren = await this.ensureQueryChildren(rootQueries);
|
|
842
879
|
const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
|
|
@@ -870,7 +907,7 @@ export default class TicketsDataProvider {
|
|
|
870
907
|
private async fetchDocTypeBranches(
|
|
871
908
|
queriesWithChildren: any,
|
|
872
909
|
docRoot: any,
|
|
873
|
-
branches: DocTypeBranchConfig[]
|
|
910
|
+
branches: DocTypeBranchConfig[],
|
|
874
911
|
): Promise<Record<string, FallbackFetchOutcome>> {
|
|
875
912
|
const results: Record<string, FallbackFetchOutcome> = {};
|
|
876
913
|
const effectiveDocRoot = docRoot ?? queriesWithChildren;
|
|
@@ -884,7 +921,7 @@ export default class TicketsDataProvider {
|
|
|
884
921
|
if (branch.folderNames?.length && effectiveDocRoot) {
|
|
885
922
|
const resolvedFolder = await this.findChildFolderByPossibleNames(
|
|
886
923
|
effectiveDocRoot,
|
|
887
|
-
branch.folderNames
|
|
924
|
+
branch.folderNames,
|
|
888
925
|
);
|
|
889
926
|
if (resolvedFolder) {
|
|
890
927
|
startingFolder = resolvedFolder;
|
|
@@ -899,7 +936,7 @@ export default class TicketsDataProvider {
|
|
|
899
936
|
startingFolder,
|
|
900
937
|
branch.fetcher,
|
|
901
938
|
branch.label,
|
|
902
|
-
branch.validator
|
|
939
|
+
branch.validator,
|
|
903
940
|
);
|
|
904
941
|
|
|
905
942
|
logger.debug(`${branch.label} final folder: ${fetchOutcome.usedFolder?.name ?? '<root>'}`);
|
|
@@ -955,7 +992,7 @@ export default class TicketsDataProvider {
|
|
|
955
992
|
private async findPathToNode(
|
|
956
993
|
currentNode: any,
|
|
957
994
|
targetId: string,
|
|
958
|
-
visited: Set<string> = new Set<string>()
|
|
995
|
+
visited: Set<string> = new Set<string>(),
|
|
959
996
|
): Promise<any[] | null> {
|
|
960
997
|
if (!currentNode) {
|
|
961
998
|
return null;
|
|
@@ -989,7 +1026,7 @@ export default class TicketsDataProvider {
|
|
|
989
1026
|
|
|
990
1027
|
private async getDocTypeRoot(
|
|
991
1028
|
rootQueries: any,
|
|
992
|
-
docTypeName: string
|
|
1029
|
+
docTypeName: string,
|
|
993
1030
|
): Promise<{ root: any; found: boolean }> {
|
|
994
1031
|
if (!rootQueries) {
|
|
995
1032
|
return { root: rootQueries, found: false };
|
|
@@ -1022,7 +1059,8 @@ export default class TicketsDataProvider {
|
|
|
1022
1059
|
async GetQueryResultsFromWiql(
|
|
1023
1060
|
wiqlHref: string = '',
|
|
1024
1061
|
displayAsTable: boolean = false,
|
|
1025
|
-
testCaseToRelatedWiMap: Map<number, Set<any
|
|
1062
|
+
testCaseToRelatedWiMap: Map<number, Set<any>>,
|
|
1063
|
+
fetchAllFields: boolean = false,
|
|
1026
1064
|
): Promise<any> {
|
|
1027
1065
|
try {
|
|
1028
1066
|
if (!wiqlHref) {
|
|
@@ -1038,13 +1076,13 @@ export default class TicketsDataProvider {
|
|
|
1038
1076
|
case QueryType.OneHop:
|
|
1039
1077
|
return displayAsTable
|
|
1040
1078
|
? await this.parseDirectLinkedQueryResultForTableFormat(queryResult, testCaseToRelatedWiMap)
|
|
1041
|
-
: await this.parseTreeQueryResult(queryResult);
|
|
1079
|
+
: await this.parseTreeQueryResult(queryResult, fetchAllFields);
|
|
1042
1080
|
case QueryType.Tree:
|
|
1043
|
-
return await this.parseTreeQueryResult(queryResult);
|
|
1081
|
+
return await this.parseTreeQueryResult(queryResult, fetchAllFields);
|
|
1044
1082
|
case QueryType.Flat:
|
|
1045
1083
|
return displayAsTable
|
|
1046
1084
|
? await this.parseFlatQueryResultForTableFormat(queryResult)
|
|
1047
|
-
: await this.parseFlatQueryResult(queryResult);
|
|
1085
|
+
: await this.parseFlatQueryResult(queryResult, fetchAllFields);
|
|
1048
1086
|
default:
|
|
1049
1087
|
break;
|
|
1050
1088
|
}
|
|
@@ -1055,7 +1093,7 @@ export default class TicketsDataProvider {
|
|
|
1055
1093
|
|
|
1056
1094
|
private async parseDirectLinkedQueryResultForTableFormat(
|
|
1057
1095
|
queryResult: QueryTree,
|
|
1058
|
-
testCaseToRelatedWiMap: Map<number, Set<any
|
|
1096
|
+
testCaseToRelatedWiMap: Map<number, Set<any>>,
|
|
1059
1097
|
) {
|
|
1060
1098
|
const { columns, workItemRelations } = queryResult;
|
|
1061
1099
|
|
|
@@ -1104,17 +1142,17 @@ export default class TicketsDataProvider {
|
|
|
1104
1142
|
const allSourcePromises = Array.from(sourceIds).map((id) =>
|
|
1105
1143
|
this.limit(() => {
|
|
1106
1144
|
const relation = workItemRelations.find(
|
|
1107
|
-
(r) => (!r.source && r.target.id === id) || r.source?.id === id
|
|
1145
|
+
(r) => (!r.source && r.target.id === id) || r.source?.id === id,
|
|
1108
1146
|
);
|
|
1109
1147
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
|
|
1110
|
-
})
|
|
1148
|
+
}),
|
|
1111
1149
|
);
|
|
1112
1150
|
|
|
1113
1151
|
const allTargetPromises = Array.from(targetIds).map((id) =>
|
|
1114
1152
|
this.limit(() => {
|
|
1115
1153
|
const relation = workItemRelations.find((r) => r.target?.id === id);
|
|
1116
1154
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
|
|
1117
|
-
})
|
|
1155
|
+
}),
|
|
1118
1156
|
);
|
|
1119
1157
|
|
|
1120
1158
|
// Wait for all fetches to complete in parallel (with concurrency control)
|
|
@@ -1188,7 +1226,7 @@ export default class TicketsDataProvider {
|
|
|
1188
1226
|
private mapTestCaseToRelatedItem(
|
|
1189
1227
|
sourceWi: any,
|
|
1190
1228
|
targetWi: any,
|
|
1191
|
-
testCaseToRelatedItemMap: Map<number, Set<any
|
|
1229
|
+
testCaseToRelatedItemMap: Map<number, Set<any>>,
|
|
1192
1230
|
) {
|
|
1193
1231
|
if (sourceWi.fields['System.WorkItemType'] == 'Test Case') {
|
|
1194
1232
|
if (!testCaseToRelatedItemMap.has(sourceWi.id)) {
|
|
@@ -1230,7 +1268,7 @@ export default class TicketsDataProvider {
|
|
|
1230
1268
|
const wiSet: Set<any> = new Set();
|
|
1231
1269
|
if (workItems) {
|
|
1232
1270
|
const fetchPromises = workItems.map((workItem) =>
|
|
1233
|
-
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false))
|
|
1271
|
+
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)),
|
|
1234
1272
|
);
|
|
1235
1273
|
|
|
1236
1274
|
const fetchedWorkItems = await Promise.all(fetchPromises);
|
|
@@ -1244,7 +1282,7 @@ export default class TicketsDataProvider {
|
|
|
1244
1282
|
};
|
|
1245
1283
|
}
|
|
1246
1284
|
|
|
1247
|
-
private async parseTreeQueryResult(queryResult: QueryTree) {
|
|
1285
|
+
private async parseTreeQueryResult(queryResult: QueryTree, fetchAllFields: boolean = false) {
|
|
1248
1286
|
const { workItemRelations } = queryResult;
|
|
1249
1287
|
if (!workItemRelations) return null;
|
|
1250
1288
|
|
|
@@ -1258,11 +1296,11 @@ export default class TicketsDataProvider {
|
|
|
1258
1296
|
// This ensures nodes with non-hierarchy links are also available
|
|
1259
1297
|
for (const rel of workItemRelations) {
|
|
1260
1298
|
const t = rel.target;
|
|
1261
|
-
if (!allItems[t.id]) await this.initTreeQueryResultItem(t, allItems);
|
|
1299
|
+
if (!allItems[t.id]) await this.initTreeQueryResultItem(t, allItems, fetchAllFields);
|
|
1262
1300
|
|
|
1263
1301
|
// Also initialize source nodes if they exist
|
|
1264
1302
|
if (rel.source && !allItems[rel.source.id]) {
|
|
1265
|
-
await this.initTreeQueryResultItem(rel.source, allItems);
|
|
1303
|
+
await this.initTreeQueryResultItem(rel.source, allItems, fetchAllFields);
|
|
1266
1304
|
}
|
|
1267
1305
|
|
|
1268
1306
|
if (rel.rel === null && rel.source === null) {
|
|
@@ -1273,7 +1311,7 @@ export default class TicketsDataProvider {
|
|
|
1273
1311
|
}
|
|
1274
1312
|
}
|
|
1275
1313
|
logger.debug(
|
|
1276
|
-
`parseTreeQueryResult: Found ${rootOrder.length} roots, ${Object.keys(allItems).length} total nodes
|
|
1314
|
+
`parseTreeQueryResult: Found ${rootOrder.length} roots, ${Object.keys(allItems).length} total nodes`,
|
|
1277
1315
|
);
|
|
1278
1316
|
|
|
1279
1317
|
// Attach only forward hierarchy edges; dedupe children by id per parent
|
|
@@ -1293,11 +1331,11 @@ export default class TicketsDataProvider {
|
|
|
1293
1331
|
// Nodes should already be initialized, but double-check
|
|
1294
1332
|
if (!allItems[parentId]) {
|
|
1295
1333
|
logger.warn(`Parent ${parentId} not found, initializing now`);
|
|
1296
|
-
await this.initTreeQueryResultItem(rel.source, allItems);
|
|
1334
|
+
await this.initTreeQueryResultItem(rel.source, allItems, fetchAllFields);
|
|
1297
1335
|
}
|
|
1298
1336
|
if (!allItems[childId]) {
|
|
1299
1337
|
logger.warn(`Child ${childId} not found, initializing now`);
|
|
1300
|
-
await this.initTreeQueryResultItem(rel.target, allItems);
|
|
1338
|
+
await this.initTreeQueryResultItem(rel.target, allItems, fetchAllFields);
|
|
1301
1339
|
}
|
|
1302
1340
|
|
|
1303
1341
|
const parent = allItems[parentId];
|
|
@@ -1313,13 +1351,13 @@ export default class TicketsDataProvider {
|
|
|
1313
1351
|
}
|
|
1314
1352
|
}
|
|
1315
1353
|
logger.debug(
|
|
1316
|
-
`parseTreeQueryResult: ${hierarchyCount} hierarchy links, ${skippedNonHierarchy} non-hierarchy links skipped
|
|
1354
|
+
`parseTreeQueryResult: ${hierarchyCount} hierarchy links, ${skippedNonHierarchy} non-hierarchy links skipped`,
|
|
1317
1355
|
);
|
|
1318
1356
|
|
|
1319
1357
|
// Return roots in original order, excluding those that became children
|
|
1320
1358
|
const roots = rootOrder.filter((id) => rootSet.has(id)).map((id) => allItems[id]);
|
|
1321
1359
|
logger.debug(
|
|
1322
|
-
`parseTreeQueryResult: Returning ${roots.length} roots with ${Object.keys(allItems).length} total items
|
|
1360
|
+
`parseTreeQueryResult: Returning ${roots.length} roots with ${Object.keys(allItems).length} total items`,
|
|
1323
1361
|
);
|
|
1324
1362
|
|
|
1325
1363
|
// Optional: clean helper sets
|
|
@@ -1332,32 +1370,42 @@ export default class TicketsDataProvider {
|
|
|
1332
1370
|
};
|
|
1333
1371
|
}
|
|
1334
1372
|
|
|
1335
|
-
private async initTreeQueryResultItem(item: any, allItems: any) {
|
|
1336
|
-
const urlWi = `${item.url}?fields
|
|
1373
|
+
private async initTreeQueryResultItem(item: any, allItems: any, fetchAllFields: boolean = false) {
|
|
1374
|
+
const urlWi = fetchAllFields ? `${item.url}` : `${item.url}?fields=${WI_DEFAULT_FIELDS}`;
|
|
1337
1375
|
const wi = await TFSServices.getItemContent(urlWi, this.token);
|
|
1376
|
+
const fields = wi?.fields || {};
|
|
1338
1377
|
// need to fetch the WI with only the the title, the web URL and the description
|
|
1339
1378
|
allItems[item.id] = {
|
|
1340
1379
|
id: item.id,
|
|
1341
|
-
title:
|
|
1342
|
-
description:
|
|
1380
|
+
title: fields['System.Title'] || '',
|
|
1381
|
+
description: fields['Microsoft.VSTS.CMMI.Symptom'] ?? fields['System.Description'] ?? '',
|
|
1343
1382
|
htmlUrl: wi._links.html.href,
|
|
1383
|
+
fields,
|
|
1384
|
+
workItemType: fields['System.WorkItemType'] || '',
|
|
1344
1385
|
children: [],
|
|
1345
1386
|
};
|
|
1346
1387
|
}
|
|
1347
1388
|
|
|
1348
|
-
private async initFlatQueryResultItem(
|
|
1349
|
-
|
|
1389
|
+
private async initFlatQueryResultItem(
|
|
1390
|
+
item: any,
|
|
1391
|
+
workItemMap: Map<number, any>,
|
|
1392
|
+
fetchAllFields: boolean = false,
|
|
1393
|
+
) {
|
|
1394
|
+
const urlWi = fetchAllFields ? `${item.url}` : `${item.url}?fields=${WI_DEFAULT_FIELDS}`;
|
|
1350
1395
|
const wi = await TFSServices.getItemContent(urlWi, this.token);
|
|
1396
|
+
const fields = wi?.fields || {};
|
|
1351
1397
|
// need to fetch the WI with only the the title, the web URL and the description
|
|
1352
1398
|
workItemMap.set(item.id, {
|
|
1353
1399
|
id: item.id,
|
|
1354
|
-
title:
|
|
1355
|
-
description:
|
|
1400
|
+
title: fields['System.Title'] || '',
|
|
1401
|
+
description: fields['Microsoft.VSTS.CMMI.Symptom'] ?? fields['System.Description'] ?? '',
|
|
1356
1402
|
htmlUrl: wi._links.html.href,
|
|
1403
|
+
fields,
|
|
1404
|
+
workItemType: fields['System.WorkItemType'] || '',
|
|
1357
1405
|
});
|
|
1358
1406
|
}
|
|
1359
1407
|
|
|
1360
|
-
private async parseFlatQueryResult(queryResult: QueryTree) {
|
|
1408
|
+
private async parseFlatQueryResult(queryResult: QueryTree, fetchAllFields: boolean = false) {
|
|
1361
1409
|
const { workItems } = queryResult;
|
|
1362
1410
|
if (!workItems) {
|
|
1363
1411
|
logger.warn(`No work items were found for this requested query`);
|
|
@@ -1368,7 +1416,7 @@ export default class TicketsDataProvider {
|
|
|
1368
1416
|
const workItemsResultMap: Map<number, any> = new Map();
|
|
1369
1417
|
for (const wi of workItems) {
|
|
1370
1418
|
if (!workItemsResultMap.has(wi.id)) {
|
|
1371
|
-
await this.initFlatQueryResultItem(wi, workItemsResultMap);
|
|
1419
|
+
await this.initFlatQueryResultItem(wi, workItemsResultMap, fetchAllFields);
|
|
1372
1420
|
}
|
|
1373
1421
|
}
|
|
1374
1422
|
return [...workItemsResultMap.values()];
|
|
@@ -1381,7 +1429,7 @@ export default class TicketsDataProvider {
|
|
|
1381
1429
|
receivedObject: any,
|
|
1382
1430
|
columnMap: Map<string, string>,
|
|
1383
1431
|
resultedRefNameMap: Map<string, string>,
|
|
1384
|
-
isRelation: boolean
|
|
1432
|
+
isRelation: boolean,
|
|
1385
1433
|
) {
|
|
1386
1434
|
const url = isRelation ? `${receivedObject.target.url}` : `${receivedObject.url}`;
|
|
1387
1435
|
const wi: any = await TFSServices.getItemContent(url, this.token);
|
|
@@ -1414,7 +1462,7 @@ export default class TicketsDataProvider {
|
|
|
1414
1462
|
if (modeledResult.queryType == 'tree') {
|
|
1415
1463
|
let levelResults: Array<Workitem> = Helper.LevelBuilder(
|
|
1416
1464
|
modeledResult,
|
|
1417
|
-
modeledResult.workItems[0].fields[0].value
|
|
1465
|
+
modeledResult.workItems[0].fields[0].value,
|
|
1418
1466
|
);
|
|
1419
1467
|
return levelResults;
|
|
1420
1468
|
}
|
|
@@ -1585,7 +1633,7 @@ export default class TicketsDataProvider {
|
|
|
1585
1633
|
|
|
1586
1634
|
async CreateNewWorkItem(projectName: string, wiBody: any, wiType: string, byPass: boolean) {
|
|
1587
1635
|
let url = `${this.orgUrl}${projectName}/_apis/wit/workitems/$${wiType}?bypassRules=${String(
|
|
1588
|
-
byPass
|
|
1636
|
+
byPass,
|
|
1589
1637
|
).toString()}`;
|
|
1590
1638
|
return TFSServices.getItemContent(url, this.token, 'POST', wiBody, {
|
|
1591
1639
|
'Content-Type': 'application/json-patch+json',
|
|
@@ -1605,7 +1653,7 @@ export default class TicketsDataProvider {
|
|
|
1605
1653
|
attachment.downloadUrl = `${relation.url}/${relation.attributes.name}`;
|
|
1606
1654
|
attachmentList.push(attachment);
|
|
1607
1655
|
}
|
|
1608
|
-
})
|
|
1656
|
+
}),
|
|
1609
1657
|
);
|
|
1610
1658
|
return attachmentList;
|
|
1611
1659
|
} catch (e) {
|
|
@@ -1626,7 +1674,7 @@ export default class TicketsDataProvider {
|
|
|
1626
1674
|
async UpdateWorkItem(projectName: string, wiBody: any, workItemId: number, byPass: boolean) {
|
|
1627
1675
|
let res: any;
|
|
1628
1676
|
let url: string = `${this.orgUrl}${projectName}/_apis/wit/workitems/${workItemId}?bypassRules=${String(
|
|
1629
|
-
byPass
|
|
1677
|
+
byPass,
|
|
1630
1678
|
).toString()}`;
|
|
1631
1679
|
res = await TFSServices.getItemContent(url, this.token, 'patch', wiBody, {
|
|
1632
1680
|
'Content-Type': 'application/json-patch+json',
|
|
@@ -1680,7 +1728,7 @@ export default class TicketsDataProvider {
|
|
|
1680
1728
|
|
|
1681
1729
|
// Process children recursively
|
|
1682
1730
|
const childResults = await Promise.all(
|
|
1683
|
-
rootQuery.children.map((child: any) => this.structureAllQueryPath(child, rootQuery.id))
|
|
1731
|
+
rootQuery.children.map((child: any) => this.structureAllQueryPath(child, rootQuery.id)),
|
|
1684
1732
|
);
|
|
1685
1733
|
|
|
1686
1734
|
// Build tree
|
|
@@ -1718,8 +1766,8 @@ export default class TicketsDataProvider {
|
|
|
1718
1766
|
} catch (err: any) {
|
|
1719
1767
|
logger.error(
|
|
1720
1768
|
`Error occurred while constructing the query list ${err.message} with query ${JSON.stringify(
|
|
1721
|
-
rootQuery
|
|
1722
|
-
)}
|
|
1769
|
+
rootQuery,
|
|
1770
|
+
)}`,
|
|
1723
1771
|
);
|
|
1724
1772
|
throw err;
|
|
1725
1773
|
}
|
|
@@ -1757,7 +1805,7 @@ export default class TicketsDataProvider {
|
|
|
1757
1805
|
includeTreeQueries: boolean = false,
|
|
1758
1806
|
excludedFolderNames: string[] = [],
|
|
1759
1807
|
includeFlatQueries: boolean = false,
|
|
1760
|
-
workItemTypeCache?: Map<string, string | null
|
|
1808
|
+
workItemTypeCache?: Map<string, string | null>,
|
|
1761
1809
|
): Promise<any> {
|
|
1762
1810
|
try {
|
|
1763
1811
|
// Per-invocation cache for ID->WorkItemType lookups; avoids global state and is safe for concurrency.
|
|
@@ -1765,7 +1813,7 @@ export default class TicketsDataProvider {
|
|
|
1765
1813
|
const shouldSkipFolder =
|
|
1766
1814
|
rootQuery?.isFolder &&
|
|
1767
1815
|
excludedFolderNames.some(
|
|
1768
|
-
(folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase()
|
|
1816
|
+
(folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase(),
|
|
1769
1817
|
);
|
|
1770
1818
|
|
|
1771
1819
|
if (shouldSkipFolder) {
|
|
@@ -1789,7 +1837,7 @@ export default class TicketsDataProvider {
|
|
|
1789
1837
|
rootQuery,
|
|
1790
1838
|
wiql,
|
|
1791
1839
|
allTypes,
|
|
1792
|
-
typeCache
|
|
1840
|
+
typeCache,
|
|
1793
1841
|
);
|
|
1794
1842
|
|
|
1795
1843
|
if (typesOk) {
|
|
@@ -1816,7 +1864,7 @@ export default class TicketsDataProvider {
|
|
|
1816
1864
|
wiql,
|
|
1817
1865
|
sources,
|
|
1818
1866
|
targets,
|
|
1819
|
-
typeCache
|
|
1867
|
+
typeCache,
|
|
1820
1868
|
);
|
|
1821
1869
|
}
|
|
1822
1870
|
const matchesReverse = await this.matchesSourceTargetConditionAsync(
|
|
@@ -1824,7 +1872,7 @@ export default class TicketsDataProvider {
|
|
|
1824
1872
|
wiql,
|
|
1825
1873
|
targets,
|
|
1826
1874
|
sources,
|
|
1827
|
-
typeCache
|
|
1875
|
+
typeCache,
|
|
1828
1876
|
);
|
|
1829
1877
|
|
|
1830
1878
|
if (matchesForward) {
|
|
@@ -1871,7 +1919,7 @@ export default class TicketsDataProvider {
|
|
|
1871
1919
|
includeTreeQueries,
|
|
1872
1920
|
excludedFolderNames,
|
|
1873
1921
|
includeFlatQueries,
|
|
1874
|
-
typeCache
|
|
1922
|
+
typeCache,
|
|
1875
1923
|
);
|
|
1876
1924
|
}
|
|
1877
1925
|
|
|
@@ -1889,9 +1937,9 @@ export default class TicketsDataProvider {
|
|
|
1889
1937
|
includeTreeQueries,
|
|
1890
1938
|
excludedFolderNames,
|
|
1891
1939
|
includeFlatQueries,
|
|
1892
|
-
typeCache
|
|
1893
|
-
)
|
|
1894
|
-
)
|
|
1940
|
+
typeCache,
|
|
1941
|
+
),
|
|
1942
|
+
),
|
|
1895
1943
|
);
|
|
1896
1944
|
|
|
1897
1945
|
// Build tree1
|
|
@@ -1924,8 +1972,8 @@ export default class TicketsDataProvider {
|
|
|
1924
1972
|
} catch (err: any) {
|
|
1925
1973
|
logger.error(
|
|
1926
1974
|
`Error occurred while constructing the query list ${err.message} with query ${JSON.stringify(
|
|
1927
|
-
rootQuery
|
|
1928
|
-
)}
|
|
1975
|
+
rootQuery,
|
|
1976
|
+
)}`,
|
|
1929
1977
|
);
|
|
1930
1978
|
logger.error(`Error stack ${err.message}`);
|
|
1931
1979
|
}
|
|
@@ -1941,7 +1989,7 @@ export default class TicketsDataProvider {
|
|
|
1941
1989
|
private matchesAreaPathCondition(
|
|
1942
1990
|
wiql: string,
|
|
1943
1991
|
sourceAreaFilter: string,
|
|
1944
|
-
targetAreaFilter: string
|
|
1992
|
+
targetAreaFilter: string,
|
|
1945
1993
|
): boolean {
|
|
1946
1994
|
const wiqlLower = (wiql || '').toLowerCase();
|
|
1947
1995
|
const srcFilter = (sourceAreaFilter || '').toLowerCase().trim();
|
|
@@ -1979,7 +2027,7 @@ export default class TicketsDataProvider {
|
|
|
1979
2027
|
wiql: string,
|
|
1980
2028
|
source: string[],
|
|
1981
2029
|
target: string[],
|
|
1982
|
-
workItemTypeCache: Map<string, string | null
|
|
2030
|
+
workItemTypeCache: Map<string, string | null>,
|
|
1983
2031
|
): Promise<boolean> {
|
|
1984
2032
|
/**
|
|
1985
2033
|
* Matches source+target constraints for link WIQL.
|
|
@@ -1992,7 +2040,7 @@ export default class TicketsDataProvider {
|
|
|
1992
2040
|
wiql,
|
|
1993
2041
|
'Source',
|
|
1994
2042
|
source,
|
|
1995
|
-
workItemTypeCache
|
|
2043
|
+
workItemTypeCache,
|
|
1996
2044
|
);
|
|
1997
2045
|
if (!sourceOk) return false;
|
|
1998
2046
|
const targetOk = await this.isLinkSideAllowedByTypeOrId(
|
|
@@ -2000,7 +2048,7 @@ export default class TicketsDataProvider {
|
|
|
2000
2048
|
wiql,
|
|
2001
2049
|
'Target',
|
|
2002
2050
|
target,
|
|
2003
|
-
workItemTypeCache
|
|
2051
|
+
workItemTypeCache,
|
|
2004
2052
|
);
|
|
2005
2053
|
return targetOk;
|
|
2006
2054
|
}
|
|
@@ -2009,7 +2057,7 @@ export default class TicketsDataProvider {
|
|
|
2009
2057
|
queryNode: any,
|
|
2010
2058
|
wiql: string,
|
|
2011
2059
|
allowedTypes: string[],
|
|
2012
|
-
workItemTypeCache: Map<string, string | null
|
|
2060
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2013
2061
|
): Promise<boolean> {
|
|
2014
2062
|
return this.isFlatQueryAllowedByTypeOrId(queryNode, wiql, allowedTypes, workItemTypeCache);
|
|
2015
2063
|
}
|
|
@@ -2149,7 +2197,7 @@ export default class TicketsDataProvider {
|
|
|
2149
2197
|
wiql: string,
|
|
2150
2198
|
context: 'Source' | 'Target',
|
|
2151
2199
|
allowedTypes: string[],
|
|
2152
|
-
workItemTypeCache: Map<string, string | null
|
|
2200
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2153
2201
|
): Promise<boolean> {
|
|
2154
2202
|
const wiqlStr = String(wiql || '');
|
|
2155
2203
|
|
|
@@ -2157,7 +2205,7 @@ export default class TicketsDataProvider {
|
|
|
2157
2205
|
if (!allowedTypes || allowedTypes.length === 0) {
|
|
2158
2206
|
const fieldPresenceRegex = new RegExp(
|
|
2159
2207
|
`${this.buildWiqlFieldPattern(context, 'System.WorkItemType')}`,
|
|
2160
|
-
'i'
|
|
2208
|
+
'i',
|
|
2161
2209
|
);
|
|
2162
2210
|
return fieldPresenceRegex.test(wiqlStr);
|
|
2163
2211
|
}
|
|
@@ -2197,7 +2245,7 @@ export default class TicketsDataProvider {
|
|
|
2197
2245
|
queryNode: any,
|
|
2198
2246
|
wiql: string,
|
|
2199
2247
|
allowedTypes: string[],
|
|
2200
|
-
workItemTypeCache: Map<string, string | null
|
|
2248
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2201
2249
|
): Promise<boolean> {
|
|
2202
2250
|
const wiqlStr = String(wiql || '');
|
|
2203
2251
|
|
|
@@ -2240,7 +2288,7 @@ export default class TicketsDataProvider {
|
|
|
2240
2288
|
private async getWorkItemTypeById(
|
|
2241
2289
|
project: string,
|
|
2242
2290
|
id: string,
|
|
2243
|
-
workItemTypeCache: Map<string, string | null
|
|
2291
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2244
2292
|
): Promise<string | null> {
|
|
2245
2293
|
const cacheKey = `${project}:${id}`;
|
|
2246
2294
|
if (workItemTypeCache.has(cacheKey)) {
|
|
@@ -2298,7 +2346,7 @@ export default class TicketsDataProvider {
|
|
|
2298
2346
|
private filterFieldsByColumns(
|
|
2299
2347
|
item: any,
|
|
2300
2348
|
columnsToFilterMap: Map<string, string>,
|
|
2301
|
-
resultedRefNameMap: Map<string, string
|
|
2349
|
+
resultedRefNameMap: Map<string, string>,
|
|
2302
2350
|
) {
|
|
2303
2351
|
try {
|
|
2304
2352
|
const parsedFields: any = {};
|
|
@@ -2345,14 +2393,14 @@ export default class TicketsDataProvider {
|
|
|
2345
2393
|
this.token,
|
|
2346
2394
|
'get',
|
|
2347
2395
|
{},
|
|
2348
|
-
{ Accept: accept }
|
|
2396
|
+
{ Accept: accept },
|
|
2349
2397
|
);
|
|
2350
2398
|
if (iconDataUrl) break;
|
|
2351
2399
|
} catch (error: any) {
|
|
2352
2400
|
logger.warn(
|
|
2353
2401
|
`Failed to download icon (${accept}) for work item type ${
|
|
2354
2402
|
workItemType?.name ?? 'unknown'
|
|
2355
|
-
}: ${error?.message || error}
|
|
2403
|
+
}: ${error?.message || error}`,
|
|
2356
2404
|
);
|
|
2357
2405
|
}
|
|
2358
2406
|
}
|
|
@@ -2361,8 +2409,8 @@ export default class TicketsDataProvider {
|
|
|
2361
2409
|
const iconPayload = workItemType.icon
|
|
2362
2410
|
? { ...workItemType.icon, dataUrl: iconDataUrl }
|
|
2363
2411
|
: iconDataUrl
|
|
2364
|
-
|
|
2365
|
-
|
|
2412
|
+
? { id: undefined, url: undefined, dataUrl: iconDataUrl }
|
|
2413
|
+
: workItemType.icon;
|
|
2366
2414
|
|
|
2367
2415
|
return {
|
|
2368
2416
|
name: workItemType.name,
|
|
@@ -2371,7 +2419,7 @@ export default class TicketsDataProvider {
|
|
|
2371
2419
|
icon: iconPayload,
|
|
2372
2420
|
states: workItemType.states,
|
|
2373
2421
|
};
|
|
2374
|
-
})
|
|
2422
|
+
}),
|
|
2375
2423
|
);
|
|
2376
2424
|
|
|
2377
2425
|
return workItemTypesWithIcons;
|
|
@@ -2543,7 +2591,7 @@ export default class TicketsDataProvider {
|
|
|
2543
2591
|
});
|
|
2544
2592
|
|
|
2545
2593
|
logger.debug(
|
|
2546
|
-
`Categorized ${workItemIds.length} work items into ${Object.keys(finalCategories).length} categories
|
|
2594
|
+
`Categorized ${workItemIds.length} work items into ${Object.keys(finalCategories).length} categories`,
|
|
2547
2595
|
);
|
|
2548
2596
|
|
|
2549
2597
|
return {
|