@elisra-devops/docgen-data-provider 1.104.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/helpers/helper.d.ts +2 -1
- package/bin/helpers/helper.js +5 -4
- package/bin/helpers/helper.js.map +1 -1
- package/bin/models/tfs-data.d.ts +1 -0
- package/bin/models/tfs-data.js.map +1 -1
- package/bin/modules/TestDataProvider.d.ts +4 -0
- package/bin/modules/TestDataProvider.js +99 -14
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +2 -1
- package/bin/modules/TicketsDataProvider.js +71 -24
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +2 -1
- package/bin/tests/helpers/helper.test.js.map +1 -1
- package/bin/tests/modules/testDataProvider.test.js +162 -9
- package/bin/tests/modules/testDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +194 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/helper.ts +19 -4
- package/src/models/tfs-data.ts +1 -0
- package/src/modules/TestDataProvider.ts +130 -22
- package/src/modules/TicketsDataProvider.ts +156 -95
- package/src/tests/helpers/helper.test.ts +2 -1
- package/src/tests/modules/testDataProvider.test.ts +225 -9
- package/src/tests/modules/ticketsDataProvider.test.ts +284 -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 = '';
|
|
@@ -199,12 +203,24 @@ export default class TicketsDataProvider {
|
|
|
199
203
|
const queriesWithChildren = await this.ensureQueryChildren(queries);
|
|
200
204
|
|
|
201
205
|
switch (normalizedDocType) {
|
|
202
|
-
case 'std':
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
case 'std':
|
|
207
|
+
case 'stp': {
|
|
208
|
+
const rootCandidates = normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
|
|
209
|
+
let stdRoot = queriesWithChildren;
|
|
210
|
+
let stdRootFound = false;
|
|
211
|
+
for (const candidate of rootCandidates) {
|
|
212
|
+
const lookup = await this.getDocTypeRoot(queriesWithChildren, candidate);
|
|
213
|
+
if (lookup.found) {
|
|
214
|
+
stdRoot = lookup.root;
|
|
215
|
+
stdRootFound = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
logger.debug(
|
|
220
|
+
`[GetSharedQueries][${normalizedDocType}] using ${
|
|
221
|
+
stdRootFound ? 'dedicated folder' : 'root queries'
|
|
222
|
+
}`,
|
|
206
223
|
);
|
|
207
|
-
logger.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
208
224
|
// Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
|
|
209
225
|
const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
|
|
210
226
|
{
|
|
@@ -258,7 +274,7 @@ export default class TicketsDataProvider {
|
|
|
258
274
|
case 'str': {
|
|
259
275
|
const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(
|
|
260
276
|
queriesWithChildren,
|
|
261
|
-
'str'
|
|
277
|
+
'str',
|
|
262
278
|
);
|
|
263
279
|
logger.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
264
280
|
const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
|
|
@@ -325,12 +341,12 @@ export default class TicketsDataProvider {
|
|
|
325
341
|
case 'test-reporter': {
|
|
326
342
|
const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(
|
|
327
343
|
queriesWithChildren,
|
|
328
|
-
'test-reporter'
|
|
344
|
+
'test-reporter',
|
|
329
345
|
);
|
|
330
346
|
logger.debug(
|
|
331
347
|
`[GetSharedQueries][test-reporter] using ${
|
|
332
348
|
testReporterFound ? 'dedicated folder' : 'root queries'
|
|
333
|
-
}
|
|
349
|
+
}`,
|
|
334
350
|
);
|
|
335
351
|
const testReporterBranches = await this.fetchDocTypeBranches(
|
|
336
352
|
queriesWithChildren,
|
|
@@ -343,13 +359,15 @@ export default class TicketsDataProvider {
|
|
|
343
359
|
fetcher: (folder: any) => this.fetchTestReporterQueries(folder),
|
|
344
360
|
validator: (result: any) => this.hasAnyQueryTree(result?.testAssociatedTree),
|
|
345
361
|
},
|
|
346
|
-
]
|
|
362
|
+
],
|
|
347
363
|
);
|
|
348
364
|
const testReporterFetch = testReporterBranches['testReporter'];
|
|
349
365
|
return testReporterFetch?.result ?? { testAssociatedTree: null };
|
|
350
366
|
}
|
|
351
367
|
case 'srs':
|
|
352
368
|
return await this.fetchSrsQueries(queriesWithChildren);
|
|
369
|
+
case 'sysrs':
|
|
370
|
+
return await this.fetchSysRsQueries(queriesWithChildren);
|
|
353
371
|
case 'svd': {
|
|
354
372
|
const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
|
|
355
373
|
if (!found) {
|
|
@@ -416,7 +434,7 @@ export default class TicketsDataProvider {
|
|
|
416
434
|
field.name !== 'Title' &&
|
|
417
435
|
field.name !== 'Description' &&
|
|
418
436
|
field.name !== 'Work Item Type' &&
|
|
419
|
-
field.name !== 'Steps'
|
|
437
|
+
field.name !== 'Steps',
|
|
420
438
|
)
|
|
421
439
|
.map((field: any) => {
|
|
422
440
|
return {
|
|
@@ -443,7 +461,7 @@ export default class TicketsDataProvider {
|
|
|
443
461
|
onlyTestReq,
|
|
444
462
|
null,
|
|
445
463
|
['Requirement'],
|
|
446
|
-
['Test Case']
|
|
464
|
+
['Test Case'],
|
|
447
465
|
);
|
|
448
466
|
return { reqTestTree, testReqTree };
|
|
449
467
|
}
|
|
@@ -476,7 +494,7 @@ export default class TicketsDataProvider {
|
|
|
476
494
|
'Review',
|
|
477
495
|
'Test Plan',
|
|
478
496
|
'Test Suite',
|
|
479
|
-
]
|
|
497
|
+
],
|
|
480
498
|
);
|
|
481
499
|
return { linkedMomTree };
|
|
482
500
|
}
|
|
@@ -532,7 +550,7 @@ export default class TicketsDataProvider {
|
|
|
532
550
|
onlySourceSide,
|
|
533
551
|
null,
|
|
534
552
|
['Bug', 'Change Request'],
|
|
535
|
-
['Test Case']
|
|
553
|
+
['Test Case'],
|
|
536
554
|
);
|
|
537
555
|
return { OpenPcrToTestTree, TestToOpenPcrTree };
|
|
538
556
|
}
|
|
@@ -548,7 +566,7 @@ export default class TicketsDataProvider {
|
|
|
548
566
|
true,
|
|
549
567
|
null,
|
|
550
568
|
['Requirement', 'Bug', 'Change Request'],
|
|
551
|
-
['Test Case']
|
|
569
|
+
['Test Case'],
|
|
552
570
|
);
|
|
553
571
|
return { testAssociatedTree };
|
|
554
572
|
}
|
|
@@ -578,7 +596,7 @@ export default class TicketsDataProvider {
|
|
|
578
596
|
undefined,
|
|
579
597
|
true, // Enable processing of both tree and direct link queries, including flat queries
|
|
580
598
|
excludedFolderNames,
|
|
581
|
-
true
|
|
599
|
+
true,
|
|
582
600
|
);
|
|
583
601
|
return { systemRequirementsQueryTree };
|
|
584
602
|
}
|
|
@@ -604,19 +622,17 @@ export default class TicketsDataProvider {
|
|
|
604
622
|
|
|
605
623
|
const systemToSoftwareFolder = await this.findChildFolderByName(
|
|
606
624
|
srsFolderWithChildren,
|
|
607
|
-
'System to Software'
|
|
625
|
+
'System to Software',
|
|
608
626
|
);
|
|
609
627
|
const softwareToSystemFolder = await this.findChildFolderByName(
|
|
610
628
|
srsFolderWithChildren,
|
|
611
|
-
'Software to System'
|
|
629
|
+
'Software to System',
|
|
612
630
|
);
|
|
613
631
|
|
|
614
|
-
const systemToSoftwareRequirementsQueries =
|
|
615
|
-
systemToSoftwareFolder
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
softwareToSystemFolder
|
|
619
|
-
);
|
|
632
|
+
const systemToSoftwareRequirementsQueries =
|
|
633
|
+
await this.fetchRequirementsTraceQueriesForFolder(systemToSoftwareFolder);
|
|
634
|
+
const softwareToSystemRequirementsQueries =
|
|
635
|
+
await this.fetchRequirementsTraceQueriesForFolder(softwareToSystemFolder);
|
|
620
636
|
|
|
621
637
|
return {
|
|
622
638
|
systemRequirementsQueries,
|
|
@@ -625,6 +641,40 @@ export default class TicketsDataProvider {
|
|
|
625
641
|
};
|
|
626
642
|
}
|
|
627
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
|
+
|
|
628
678
|
/**
|
|
629
679
|
* Fetches and structures linked queries related to requirements traceability with area path filtering.
|
|
630
680
|
*
|
|
@@ -649,7 +699,7 @@ export default class TicketsDataProvider {
|
|
|
649
699
|
['epic', 'feature', 'requirement'],
|
|
650
700
|
['epic', 'feature', 'requirement'],
|
|
651
701
|
'sys', // Source area filter for tree1: System area paths
|
|
652
|
-
'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)
|
|
653
703
|
);
|
|
654
704
|
return { SystemToSoftwareRequirementsTree, SoftwareToSystemRequirementsTree };
|
|
655
705
|
}
|
|
@@ -667,7 +717,7 @@ export default class TicketsDataProvider {
|
|
|
667
717
|
['epic', 'feature', 'requirement'],
|
|
668
718
|
undefined,
|
|
669
719
|
undefined,
|
|
670
|
-
true
|
|
720
|
+
true,
|
|
671
721
|
);
|
|
672
722
|
return tree1;
|
|
673
723
|
}
|
|
@@ -714,7 +764,7 @@ export default class TicketsDataProvider {
|
|
|
714
764
|
const normalizedName = childName.toLowerCase();
|
|
715
765
|
return (
|
|
716
766
|
parentWithChildren.children.find(
|
|
717
|
-
(child: any) => child.isFolder && (child.name || '').toLowerCase() === normalizedName
|
|
767
|
+
(child: any) => child.isFolder && (child.name || '').toLowerCase() === normalizedName,
|
|
718
768
|
) || null
|
|
719
769
|
);
|
|
720
770
|
}
|
|
@@ -823,7 +873,7 @@ export default class TicketsDataProvider {
|
|
|
823
873
|
startingFolder: any,
|
|
824
874
|
fetcher: (folder: any) => Promise<any>,
|
|
825
875
|
logContext: string,
|
|
826
|
-
validator?: (result: any) => boolean
|
|
876
|
+
validator?: (result: any) => boolean,
|
|
827
877
|
): Promise<{ result: any; usedFolder: any }> {
|
|
828
878
|
const rootWithChildren = await this.ensureQueryChildren(rootQueries);
|
|
829
879
|
const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
|
|
@@ -857,7 +907,7 @@ export default class TicketsDataProvider {
|
|
|
857
907
|
private async fetchDocTypeBranches(
|
|
858
908
|
queriesWithChildren: any,
|
|
859
909
|
docRoot: any,
|
|
860
|
-
branches: DocTypeBranchConfig[]
|
|
910
|
+
branches: DocTypeBranchConfig[],
|
|
861
911
|
): Promise<Record<string, FallbackFetchOutcome>> {
|
|
862
912
|
const results: Record<string, FallbackFetchOutcome> = {};
|
|
863
913
|
const effectiveDocRoot = docRoot ?? queriesWithChildren;
|
|
@@ -871,7 +921,7 @@ export default class TicketsDataProvider {
|
|
|
871
921
|
if (branch.folderNames?.length && effectiveDocRoot) {
|
|
872
922
|
const resolvedFolder = await this.findChildFolderByPossibleNames(
|
|
873
923
|
effectiveDocRoot,
|
|
874
|
-
branch.folderNames
|
|
924
|
+
branch.folderNames,
|
|
875
925
|
);
|
|
876
926
|
if (resolvedFolder) {
|
|
877
927
|
startingFolder = resolvedFolder;
|
|
@@ -886,7 +936,7 @@ export default class TicketsDataProvider {
|
|
|
886
936
|
startingFolder,
|
|
887
937
|
branch.fetcher,
|
|
888
938
|
branch.label,
|
|
889
|
-
branch.validator
|
|
939
|
+
branch.validator,
|
|
890
940
|
);
|
|
891
941
|
|
|
892
942
|
logger.debug(`${branch.label} final folder: ${fetchOutcome.usedFolder?.name ?? '<root>'}`);
|
|
@@ -942,7 +992,7 @@ export default class TicketsDataProvider {
|
|
|
942
992
|
private async findPathToNode(
|
|
943
993
|
currentNode: any,
|
|
944
994
|
targetId: string,
|
|
945
|
-
visited: Set<string> = new Set<string>()
|
|
995
|
+
visited: Set<string> = new Set<string>(),
|
|
946
996
|
): Promise<any[] | null> {
|
|
947
997
|
if (!currentNode) {
|
|
948
998
|
return null;
|
|
@@ -976,7 +1026,7 @@ export default class TicketsDataProvider {
|
|
|
976
1026
|
|
|
977
1027
|
private async getDocTypeRoot(
|
|
978
1028
|
rootQueries: any,
|
|
979
|
-
docTypeName: string
|
|
1029
|
+
docTypeName: string,
|
|
980
1030
|
): Promise<{ root: any; found: boolean }> {
|
|
981
1031
|
if (!rootQueries) {
|
|
982
1032
|
return { root: rootQueries, found: false };
|
|
@@ -1009,7 +1059,8 @@ export default class TicketsDataProvider {
|
|
|
1009
1059
|
async GetQueryResultsFromWiql(
|
|
1010
1060
|
wiqlHref: string = '',
|
|
1011
1061
|
displayAsTable: boolean = false,
|
|
1012
|
-
testCaseToRelatedWiMap: Map<number, Set<any
|
|
1062
|
+
testCaseToRelatedWiMap: Map<number, Set<any>>,
|
|
1063
|
+
fetchAllFields: boolean = false,
|
|
1013
1064
|
): Promise<any> {
|
|
1014
1065
|
try {
|
|
1015
1066
|
if (!wiqlHref) {
|
|
@@ -1025,13 +1076,13 @@ export default class TicketsDataProvider {
|
|
|
1025
1076
|
case QueryType.OneHop:
|
|
1026
1077
|
return displayAsTable
|
|
1027
1078
|
? await this.parseDirectLinkedQueryResultForTableFormat(queryResult, testCaseToRelatedWiMap)
|
|
1028
|
-
: await this.parseTreeQueryResult(queryResult);
|
|
1079
|
+
: await this.parseTreeQueryResult(queryResult, fetchAllFields);
|
|
1029
1080
|
case QueryType.Tree:
|
|
1030
|
-
return await this.parseTreeQueryResult(queryResult);
|
|
1081
|
+
return await this.parseTreeQueryResult(queryResult, fetchAllFields);
|
|
1031
1082
|
case QueryType.Flat:
|
|
1032
1083
|
return displayAsTable
|
|
1033
1084
|
? await this.parseFlatQueryResultForTableFormat(queryResult)
|
|
1034
|
-
: await this.parseFlatQueryResult(queryResult);
|
|
1085
|
+
: await this.parseFlatQueryResult(queryResult, fetchAllFields);
|
|
1035
1086
|
default:
|
|
1036
1087
|
break;
|
|
1037
1088
|
}
|
|
@@ -1042,7 +1093,7 @@ export default class TicketsDataProvider {
|
|
|
1042
1093
|
|
|
1043
1094
|
private async parseDirectLinkedQueryResultForTableFormat(
|
|
1044
1095
|
queryResult: QueryTree,
|
|
1045
|
-
testCaseToRelatedWiMap: Map<number, Set<any
|
|
1096
|
+
testCaseToRelatedWiMap: Map<number, Set<any>>,
|
|
1046
1097
|
) {
|
|
1047
1098
|
const { columns, workItemRelations } = queryResult;
|
|
1048
1099
|
|
|
@@ -1091,17 +1142,17 @@ export default class TicketsDataProvider {
|
|
|
1091
1142
|
const allSourcePromises = Array.from(sourceIds).map((id) =>
|
|
1092
1143
|
this.limit(() => {
|
|
1093
1144
|
const relation = workItemRelations.find(
|
|
1094
|
-
(r) => (!r.source && r.target.id === id) || r.source?.id === id
|
|
1145
|
+
(r) => (!r.source && r.target.id === id) || r.source?.id === id,
|
|
1095
1146
|
);
|
|
1096
1147
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
|
|
1097
|
-
})
|
|
1148
|
+
}),
|
|
1098
1149
|
);
|
|
1099
1150
|
|
|
1100
1151
|
const allTargetPromises = Array.from(targetIds).map((id) =>
|
|
1101
1152
|
this.limit(() => {
|
|
1102
1153
|
const relation = workItemRelations.find((r) => r.target?.id === id);
|
|
1103
1154
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
|
|
1104
|
-
})
|
|
1155
|
+
}),
|
|
1105
1156
|
);
|
|
1106
1157
|
|
|
1107
1158
|
// Wait for all fetches to complete in parallel (with concurrency control)
|
|
@@ -1175,7 +1226,7 @@ export default class TicketsDataProvider {
|
|
|
1175
1226
|
private mapTestCaseToRelatedItem(
|
|
1176
1227
|
sourceWi: any,
|
|
1177
1228
|
targetWi: any,
|
|
1178
|
-
testCaseToRelatedItemMap: Map<number, Set<any
|
|
1229
|
+
testCaseToRelatedItemMap: Map<number, Set<any>>,
|
|
1179
1230
|
) {
|
|
1180
1231
|
if (sourceWi.fields['System.WorkItemType'] == 'Test Case') {
|
|
1181
1232
|
if (!testCaseToRelatedItemMap.has(sourceWi.id)) {
|
|
@@ -1217,7 +1268,7 @@ export default class TicketsDataProvider {
|
|
|
1217
1268
|
const wiSet: Set<any> = new Set();
|
|
1218
1269
|
if (workItems) {
|
|
1219
1270
|
const fetchPromises = workItems.map((workItem) =>
|
|
1220
|
-
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false))
|
|
1271
|
+
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)),
|
|
1221
1272
|
);
|
|
1222
1273
|
|
|
1223
1274
|
const fetchedWorkItems = await Promise.all(fetchPromises);
|
|
@@ -1231,7 +1282,7 @@ export default class TicketsDataProvider {
|
|
|
1231
1282
|
};
|
|
1232
1283
|
}
|
|
1233
1284
|
|
|
1234
|
-
private async parseTreeQueryResult(queryResult: QueryTree) {
|
|
1285
|
+
private async parseTreeQueryResult(queryResult: QueryTree, fetchAllFields: boolean = false) {
|
|
1235
1286
|
const { workItemRelations } = queryResult;
|
|
1236
1287
|
if (!workItemRelations) return null;
|
|
1237
1288
|
|
|
@@ -1245,11 +1296,11 @@ export default class TicketsDataProvider {
|
|
|
1245
1296
|
// This ensures nodes with non-hierarchy links are also available
|
|
1246
1297
|
for (const rel of workItemRelations) {
|
|
1247
1298
|
const t = rel.target;
|
|
1248
|
-
if (!allItems[t.id]) await this.initTreeQueryResultItem(t, allItems);
|
|
1299
|
+
if (!allItems[t.id]) await this.initTreeQueryResultItem(t, allItems, fetchAllFields);
|
|
1249
1300
|
|
|
1250
1301
|
// Also initialize source nodes if they exist
|
|
1251
1302
|
if (rel.source && !allItems[rel.source.id]) {
|
|
1252
|
-
await this.initTreeQueryResultItem(rel.source, allItems);
|
|
1303
|
+
await this.initTreeQueryResultItem(rel.source, allItems, fetchAllFields);
|
|
1253
1304
|
}
|
|
1254
1305
|
|
|
1255
1306
|
if (rel.rel === null && rel.source === null) {
|
|
@@ -1260,7 +1311,7 @@ export default class TicketsDataProvider {
|
|
|
1260
1311
|
}
|
|
1261
1312
|
}
|
|
1262
1313
|
logger.debug(
|
|
1263
|
-
`parseTreeQueryResult: Found ${rootOrder.length} roots, ${Object.keys(allItems).length} total nodes
|
|
1314
|
+
`parseTreeQueryResult: Found ${rootOrder.length} roots, ${Object.keys(allItems).length} total nodes`,
|
|
1264
1315
|
);
|
|
1265
1316
|
|
|
1266
1317
|
// Attach only forward hierarchy edges; dedupe children by id per parent
|
|
@@ -1280,11 +1331,11 @@ export default class TicketsDataProvider {
|
|
|
1280
1331
|
// Nodes should already be initialized, but double-check
|
|
1281
1332
|
if (!allItems[parentId]) {
|
|
1282
1333
|
logger.warn(`Parent ${parentId} not found, initializing now`);
|
|
1283
|
-
await this.initTreeQueryResultItem(rel.source, allItems);
|
|
1334
|
+
await this.initTreeQueryResultItem(rel.source, allItems, fetchAllFields);
|
|
1284
1335
|
}
|
|
1285
1336
|
if (!allItems[childId]) {
|
|
1286
1337
|
logger.warn(`Child ${childId} not found, initializing now`);
|
|
1287
|
-
await this.initTreeQueryResultItem(rel.target, allItems);
|
|
1338
|
+
await this.initTreeQueryResultItem(rel.target, allItems, fetchAllFields);
|
|
1288
1339
|
}
|
|
1289
1340
|
|
|
1290
1341
|
const parent = allItems[parentId];
|
|
@@ -1300,13 +1351,13 @@ export default class TicketsDataProvider {
|
|
|
1300
1351
|
}
|
|
1301
1352
|
}
|
|
1302
1353
|
logger.debug(
|
|
1303
|
-
`parseTreeQueryResult: ${hierarchyCount} hierarchy links, ${skippedNonHierarchy} non-hierarchy links skipped
|
|
1354
|
+
`parseTreeQueryResult: ${hierarchyCount} hierarchy links, ${skippedNonHierarchy} non-hierarchy links skipped`,
|
|
1304
1355
|
);
|
|
1305
1356
|
|
|
1306
1357
|
// Return roots in original order, excluding those that became children
|
|
1307
1358
|
const roots = rootOrder.filter((id) => rootSet.has(id)).map((id) => allItems[id]);
|
|
1308
1359
|
logger.debug(
|
|
1309
|
-
`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`,
|
|
1310
1361
|
);
|
|
1311
1362
|
|
|
1312
1363
|
// Optional: clean helper sets
|
|
@@ -1319,32 +1370,42 @@ export default class TicketsDataProvider {
|
|
|
1319
1370
|
};
|
|
1320
1371
|
}
|
|
1321
1372
|
|
|
1322
|
-
private async initTreeQueryResultItem(item: any, allItems: any) {
|
|
1323
|
-
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}`;
|
|
1324
1375
|
const wi = await TFSServices.getItemContent(urlWi, this.token);
|
|
1376
|
+
const fields = wi?.fields || {};
|
|
1325
1377
|
// need to fetch the WI with only the the title, the web URL and the description
|
|
1326
1378
|
allItems[item.id] = {
|
|
1327
1379
|
id: item.id,
|
|
1328
|
-
title:
|
|
1329
|
-
description:
|
|
1380
|
+
title: fields['System.Title'] || '',
|
|
1381
|
+
description: fields['Microsoft.VSTS.CMMI.Symptom'] ?? fields['System.Description'] ?? '',
|
|
1330
1382
|
htmlUrl: wi._links.html.href,
|
|
1383
|
+
fields,
|
|
1384
|
+
workItemType: fields['System.WorkItemType'] || '',
|
|
1331
1385
|
children: [],
|
|
1332
1386
|
};
|
|
1333
1387
|
}
|
|
1334
1388
|
|
|
1335
|
-
private async initFlatQueryResultItem(
|
|
1336
|
-
|
|
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}`;
|
|
1337
1395
|
const wi = await TFSServices.getItemContent(urlWi, this.token);
|
|
1396
|
+
const fields = wi?.fields || {};
|
|
1338
1397
|
// need to fetch the WI with only the the title, the web URL and the description
|
|
1339
1398
|
workItemMap.set(item.id, {
|
|
1340
1399
|
id: item.id,
|
|
1341
|
-
title:
|
|
1342
|
-
description:
|
|
1400
|
+
title: fields['System.Title'] || '',
|
|
1401
|
+
description: fields['Microsoft.VSTS.CMMI.Symptom'] ?? fields['System.Description'] ?? '',
|
|
1343
1402
|
htmlUrl: wi._links.html.href,
|
|
1403
|
+
fields,
|
|
1404
|
+
workItemType: fields['System.WorkItemType'] || '',
|
|
1344
1405
|
});
|
|
1345
1406
|
}
|
|
1346
1407
|
|
|
1347
|
-
private async parseFlatQueryResult(queryResult: QueryTree) {
|
|
1408
|
+
private async parseFlatQueryResult(queryResult: QueryTree, fetchAllFields: boolean = false) {
|
|
1348
1409
|
const { workItems } = queryResult;
|
|
1349
1410
|
if (!workItems) {
|
|
1350
1411
|
logger.warn(`No work items were found for this requested query`);
|
|
@@ -1355,7 +1416,7 @@ export default class TicketsDataProvider {
|
|
|
1355
1416
|
const workItemsResultMap: Map<number, any> = new Map();
|
|
1356
1417
|
for (const wi of workItems) {
|
|
1357
1418
|
if (!workItemsResultMap.has(wi.id)) {
|
|
1358
|
-
await this.initFlatQueryResultItem(wi, workItemsResultMap);
|
|
1419
|
+
await this.initFlatQueryResultItem(wi, workItemsResultMap, fetchAllFields);
|
|
1359
1420
|
}
|
|
1360
1421
|
}
|
|
1361
1422
|
return [...workItemsResultMap.values()];
|
|
@@ -1368,7 +1429,7 @@ export default class TicketsDataProvider {
|
|
|
1368
1429
|
receivedObject: any,
|
|
1369
1430
|
columnMap: Map<string, string>,
|
|
1370
1431
|
resultedRefNameMap: Map<string, string>,
|
|
1371
|
-
isRelation: boolean
|
|
1432
|
+
isRelation: boolean,
|
|
1372
1433
|
) {
|
|
1373
1434
|
const url = isRelation ? `${receivedObject.target.url}` : `${receivedObject.url}`;
|
|
1374
1435
|
const wi: any = await TFSServices.getItemContent(url, this.token);
|
|
@@ -1401,7 +1462,7 @@ export default class TicketsDataProvider {
|
|
|
1401
1462
|
if (modeledResult.queryType == 'tree') {
|
|
1402
1463
|
let levelResults: Array<Workitem> = Helper.LevelBuilder(
|
|
1403
1464
|
modeledResult,
|
|
1404
|
-
modeledResult.workItems[0].fields[0].value
|
|
1465
|
+
modeledResult.workItems[0].fields[0].value,
|
|
1405
1466
|
);
|
|
1406
1467
|
return levelResults;
|
|
1407
1468
|
}
|
|
@@ -1572,7 +1633,7 @@ export default class TicketsDataProvider {
|
|
|
1572
1633
|
|
|
1573
1634
|
async CreateNewWorkItem(projectName: string, wiBody: any, wiType: string, byPass: boolean) {
|
|
1574
1635
|
let url = `${this.orgUrl}${projectName}/_apis/wit/workitems/$${wiType}?bypassRules=${String(
|
|
1575
|
-
byPass
|
|
1636
|
+
byPass,
|
|
1576
1637
|
).toString()}`;
|
|
1577
1638
|
return TFSServices.getItemContent(url, this.token, 'POST', wiBody, {
|
|
1578
1639
|
'Content-Type': 'application/json-patch+json',
|
|
@@ -1592,7 +1653,7 @@ export default class TicketsDataProvider {
|
|
|
1592
1653
|
attachment.downloadUrl = `${relation.url}/${relation.attributes.name}`;
|
|
1593
1654
|
attachmentList.push(attachment);
|
|
1594
1655
|
}
|
|
1595
|
-
})
|
|
1656
|
+
}),
|
|
1596
1657
|
);
|
|
1597
1658
|
return attachmentList;
|
|
1598
1659
|
} catch (e) {
|
|
@@ -1613,7 +1674,7 @@ export default class TicketsDataProvider {
|
|
|
1613
1674
|
async UpdateWorkItem(projectName: string, wiBody: any, workItemId: number, byPass: boolean) {
|
|
1614
1675
|
let res: any;
|
|
1615
1676
|
let url: string = `${this.orgUrl}${projectName}/_apis/wit/workitems/${workItemId}?bypassRules=${String(
|
|
1616
|
-
byPass
|
|
1677
|
+
byPass,
|
|
1617
1678
|
).toString()}`;
|
|
1618
1679
|
res = await TFSServices.getItemContent(url, this.token, 'patch', wiBody, {
|
|
1619
1680
|
'Content-Type': 'application/json-patch+json',
|
|
@@ -1667,7 +1728,7 @@ export default class TicketsDataProvider {
|
|
|
1667
1728
|
|
|
1668
1729
|
// Process children recursively
|
|
1669
1730
|
const childResults = await Promise.all(
|
|
1670
|
-
rootQuery.children.map((child: any) => this.structureAllQueryPath(child, rootQuery.id))
|
|
1731
|
+
rootQuery.children.map((child: any) => this.structureAllQueryPath(child, rootQuery.id)),
|
|
1671
1732
|
);
|
|
1672
1733
|
|
|
1673
1734
|
// Build tree
|
|
@@ -1705,8 +1766,8 @@ export default class TicketsDataProvider {
|
|
|
1705
1766
|
} catch (err: any) {
|
|
1706
1767
|
logger.error(
|
|
1707
1768
|
`Error occurred while constructing the query list ${err.message} with query ${JSON.stringify(
|
|
1708
|
-
rootQuery
|
|
1709
|
-
)}
|
|
1769
|
+
rootQuery,
|
|
1770
|
+
)}`,
|
|
1710
1771
|
);
|
|
1711
1772
|
throw err;
|
|
1712
1773
|
}
|
|
@@ -1744,7 +1805,7 @@ export default class TicketsDataProvider {
|
|
|
1744
1805
|
includeTreeQueries: boolean = false,
|
|
1745
1806
|
excludedFolderNames: string[] = [],
|
|
1746
1807
|
includeFlatQueries: boolean = false,
|
|
1747
|
-
workItemTypeCache?: Map<string, string | null
|
|
1808
|
+
workItemTypeCache?: Map<string, string | null>,
|
|
1748
1809
|
): Promise<any> {
|
|
1749
1810
|
try {
|
|
1750
1811
|
// Per-invocation cache for ID->WorkItemType lookups; avoids global state and is safe for concurrency.
|
|
@@ -1752,7 +1813,7 @@ export default class TicketsDataProvider {
|
|
|
1752
1813
|
const shouldSkipFolder =
|
|
1753
1814
|
rootQuery?.isFolder &&
|
|
1754
1815
|
excludedFolderNames.some(
|
|
1755
|
-
(folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase()
|
|
1816
|
+
(folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase(),
|
|
1756
1817
|
);
|
|
1757
1818
|
|
|
1758
1819
|
if (shouldSkipFolder) {
|
|
@@ -1776,7 +1837,7 @@ export default class TicketsDataProvider {
|
|
|
1776
1837
|
rootQuery,
|
|
1777
1838
|
wiql,
|
|
1778
1839
|
allTypes,
|
|
1779
|
-
typeCache
|
|
1840
|
+
typeCache,
|
|
1780
1841
|
);
|
|
1781
1842
|
|
|
1782
1843
|
if (typesOk) {
|
|
@@ -1803,7 +1864,7 @@ export default class TicketsDataProvider {
|
|
|
1803
1864
|
wiql,
|
|
1804
1865
|
sources,
|
|
1805
1866
|
targets,
|
|
1806
|
-
typeCache
|
|
1867
|
+
typeCache,
|
|
1807
1868
|
);
|
|
1808
1869
|
}
|
|
1809
1870
|
const matchesReverse = await this.matchesSourceTargetConditionAsync(
|
|
@@ -1811,7 +1872,7 @@ export default class TicketsDataProvider {
|
|
|
1811
1872
|
wiql,
|
|
1812
1873
|
targets,
|
|
1813
1874
|
sources,
|
|
1814
|
-
typeCache
|
|
1875
|
+
typeCache,
|
|
1815
1876
|
);
|
|
1816
1877
|
|
|
1817
1878
|
if (matchesForward) {
|
|
@@ -1858,7 +1919,7 @@ export default class TicketsDataProvider {
|
|
|
1858
1919
|
includeTreeQueries,
|
|
1859
1920
|
excludedFolderNames,
|
|
1860
1921
|
includeFlatQueries,
|
|
1861
|
-
typeCache
|
|
1922
|
+
typeCache,
|
|
1862
1923
|
);
|
|
1863
1924
|
}
|
|
1864
1925
|
|
|
@@ -1876,9 +1937,9 @@ export default class TicketsDataProvider {
|
|
|
1876
1937
|
includeTreeQueries,
|
|
1877
1938
|
excludedFolderNames,
|
|
1878
1939
|
includeFlatQueries,
|
|
1879
|
-
typeCache
|
|
1880
|
-
)
|
|
1881
|
-
)
|
|
1940
|
+
typeCache,
|
|
1941
|
+
),
|
|
1942
|
+
),
|
|
1882
1943
|
);
|
|
1883
1944
|
|
|
1884
1945
|
// Build tree1
|
|
@@ -1911,8 +1972,8 @@ export default class TicketsDataProvider {
|
|
|
1911
1972
|
} catch (err: any) {
|
|
1912
1973
|
logger.error(
|
|
1913
1974
|
`Error occurred while constructing the query list ${err.message} with query ${JSON.stringify(
|
|
1914
|
-
rootQuery
|
|
1915
|
-
)}
|
|
1975
|
+
rootQuery,
|
|
1976
|
+
)}`,
|
|
1916
1977
|
);
|
|
1917
1978
|
logger.error(`Error stack ${err.message}`);
|
|
1918
1979
|
}
|
|
@@ -1928,7 +1989,7 @@ export default class TicketsDataProvider {
|
|
|
1928
1989
|
private matchesAreaPathCondition(
|
|
1929
1990
|
wiql: string,
|
|
1930
1991
|
sourceAreaFilter: string,
|
|
1931
|
-
targetAreaFilter: string
|
|
1992
|
+
targetAreaFilter: string,
|
|
1932
1993
|
): boolean {
|
|
1933
1994
|
const wiqlLower = (wiql || '').toLowerCase();
|
|
1934
1995
|
const srcFilter = (sourceAreaFilter || '').toLowerCase().trim();
|
|
@@ -1966,7 +2027,7 @@ export default class TicketsDataProvider {
|
|
|
1966
2027
|
wiql: string,
|
|
1967
2028
|
source: string[],
|
|
1968
2029
|
target: string[],
|
|
1969
|
-
workItemTypeCache: Map<string, string | null
|
|
2030
|
+
workItemTypeCache: Map<string, string | null>,
|
|
1970
2031
|
): Promise<boolean> {
|
|
1971
2032
|
/**
|
|
1972
2033
|
* Matches source+target constraints for link WIQL.
|
|
@@ -1979,7 +2040,7 @@ export default class TicketsDataProvider {
|
|
|
1979
2040
|
wiql,
|
|
1980
2041
|
'Source',
|
|
1981
2042
|
source,
|
|
1982
|
-
workItemTypeCache
|
|
2043
|
+
workItemTypeCache,
|
|
1983
2044
|
);
|
|
1984
2045
|
if (!sourceOk) return false;
|
|
1985
2046
|
const targetOk = await this.isLinkSideAllowedByTypeOrId(
|
|
@@ -1987,7 +2048,7 @@ export default class TicketsDataProvider {
|
|
|
1987
2048
|
wiql,
|
|
1988
2049
|
'Target',
|
|
1989
2050
|
target,
|
|
1990
|
-
workItemTypeCache
|
|
2051
|
+
workItemTypeCache,
|
|
1991
2052
|
);
|
|
1992
2053
|
return targetOk;
|
|
1993
2054
|
}
|
|
@@ -1996,7 +2057,7 @@ export default class TicketsDataProvider {
|
|
|
1996
2057
|
queryNode: any,
|
|
1997
2058
|
wiql: string,
|
|
1998
2059
|
allowedTypes: string[],
|
|
1999
|
-
workItemTypeCache: Map<string, string | null
|
|
2060
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2000
2061
|
): Promise<boolean> {
|
|
2001
2062
|
return this.isFlatQueryAllowedByTypeOrId(queryNode, wiql, allowedTypes, workItemTypeCache);
|
|
2002
2063
|
}
|
|
@@ -2136,7 +2197,7 @@ export default class TicketsDataProvider {
|
|
|
2136
2197
|
wiql: string,
|
|
2137
2198
|
context: 'Source' | 'Target',
|
|
2138
2199
|
allowedTypes: string[],
|
|
2139
|
-
workItemTypeCache: Map<string, string | null
|
|
2200
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2140
2201
|
): Promise<boolean> {
|
|
2141
2202
|
const wiqlStr = String(wiql || '');
|
|
2142
2203
|
|
|
@@ -2144,7 +2205,7 @@ export default class TicketsDataProvider {
|
|
|
2144
2205
|
if (!allowedTypes || allowedTypes.length === 0) {
|
|
2145
2206
|
const fieldPresenceRegex = new RegExp(
|
|
2146
2207
|
`${this.buildWiqlFieldPattern(context, 'System.WorkItemType')}`,
|
|
2147
|
-
'i'
|
|
2208
|
+
'i',
|
|
2148
2209
|
);
|
|
2149
2210
|
return fieldPresenceRegex.test(wiqlStr);
|
|
2150
2211
|
}
|
|
@@ -2184,7 +2245,7 @@ export default class TicketsDataProvider {
|
|
|
2184
2245
|
queryNode: any,
|
|
2185
2246
|
wiql: string,
|
|
2186
2247
|
allowedTypes: string[],
|
|
2187
|
-
workItemTypeCache: Map<string, string | null
|
|
2248
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2188
2249
|
): Promise<boolean> {
|
|
2189
2250
|
const wiqlStr = String(wiql || '');
|
|
2190
2251
|
|
|
@@ -2227,7 +2288,7 @@ export default class TicketsDataProvider {
|
|
|
2227
2288
|
private async getWorkItemTypeById(
|
|
2228
2289
|
project: string,
|
|
2229
2290
|
id: string,
|
|
2230
|
-
workItemTypeCache: Map<string, string | null
|
|
2291
|
+
workItemTypeCache: Map<string, string | null>,
|
|
2231
2292
|
): Promise<string | null> {
|
|
2232
2293
|
const cacheKey = `${project}:${id}`;
|
|
2233
2294
|
if (workItemTypeCache.has(cacheKey)) {
|
|
@@ -2285,7 +2346,7 @@ export default class TicketsDataProvider {
|
|
|
2285
2346
|
private filterFieldsByColumns(
|
|
2286
2347
|
item: any,
|
|
2287
2348
|
columnsToFilterMap: Map<string, string>,
|
|
2288
|
-
resultedRefNameMap: Map<string, string
|
|
2349
|
+
resultedRefNameMap: Map<string, string>,
|
|
2289
2350
|
) {
|
|
2290
2351
|
try {
|
|
2291
2352
|
const parsedFields: any = {};
|
|
@@ -2332,14 +2393,14 @@ export default class TicketsDataProvider {
|
|
|
2332
2393
|
this.token,
|
|
2333
2394
|
'get',
|
|
2334
2395
|
{},
|
|
2335
|
-
{ Accept: accept }
|
|
2396
|
+
{ Accept: accept },
|
|
2336
2397
|
);
|
|
2337
2398
|
if (iconDataUrl) break;
|
|
2338
2399
|
} catch (error: any) {
|
|
2339
2400
|
logger.warn(
|
|
2340
2401
|
`Failed to download icon (${accept}) for work item type ${
|
|
2341
2402
|
workItemType?.name ?? 'unknown'
|
|
2342
|
-
}: ${error?.message || error}
|
|
2403
|
+
}: ${error?.message || error}`,
|
|
2343
2404
|
);
|
|
2344
2405
|
}
|
|
2345
2406
|
}
|
|
@@ -2348,8 +2409,8 @@ export default class TicketsDataProvider {
|
|
|
2348
2409
|
const iconPayload = workItemType.icon
|
|
2349
2410
|
? { ...workItemType.icon, dataUrl: iconDataUrl }
|
|
2350
2411
|
: iconDataUrl
|
|
2351
|
-
|
|
2352
|
-
|
|
2412
|
+
? { id: undefined, url: undefined, dataUrl: iconDataUrl }
|
|
2413
|
+
: workItemType.icon;
|
|
2353
2414
|
|
|
2354
2415
|
return {
|
|
2355
2416
|
name: workItemType.name,
|
|
@@ -2358,7 +2419,7 @@ export default class TicketsDataProvider {
|
|
|
2358
2419
|
icon: iconPayload,
|
|
2359
2420
|
states: workItemType.states,
|
|
2360
2421
|
};
|
|
2361
|
-
})
|
|
2422
|
+
}),
|
|
2362
2423
|
);
|
|
2363
2424
|
|
|
2364
2425
|
return workItemTypesWithIcons;
|
|
@@ -2530,7 +2591,7 @@ export default class TicketsDataProvider {
|
|
|
2530
2591
|
});
|
|
2531
2592
|
|
|
2532
2593
|
logger.debug(
|
|
2533
|
-
`Categorized ${workItemIds.length} work items into ${Object.keys(finalCategories).length} categories
|
|
2594
|
+
`Categorized ${workItemIds.length} work items into ${Object.keys(finalCategories).length} categories`,
|
|
2534
2595
|
);
|
|
2535
2596
|
|
|
2536
2597
|
return {
|