devops-plugin-kit 0.1.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.
Files changed (112) hide show
  1. package/@danieli-automation/create-devops-plugin/package.json +30 -0
  2. package/@danieli-automation/create-devops-plugin/src/cli.ts +136 -0
  3. package/@danieli-automation/create-devops-plugin/src/index.ts +37 -0
  4. package/@danieli-automation/create-devops-plugin/src/template/files/apiIndexMock.ts +10 -0
  5. package/@danieli-automation/create-devops-plugin/src/template/files/appHtml.ts +25 -0
  6. package/@danieli-automation/create-devops-plugin/src/template/files/appStyles.ts +22 -0
  7. package/@danieli-automation/create-devops-plugin/src/template/files/appTsx.ts +41 -0
  8. package/@danieli-automation/create-devops-plugin/src/template/files/envExample.ts +11 -0
  9. package/@danieli-automation/create-devops-plugin/src/template/files/gitignore.ts +13 -0
  10. package/@danieli-automation/create-devops-plugin/src/template/files/index.ts +51 -0
  11. package/@danieli-automation/create-devops-plugin/src/template/files/packageJson.ts +60 -0
  12. package/@danieli-automation/create-devops-plugin/src/template/files/readme.ts +30 -0
  13. package/@danieli-automation/create-devops-plugin/src/template/files/sdkMock.ts +11 -0
  14. package/@danieli-automation/create-devops-plugin/src/template/files/testSetup.ts +16 -0
  15. package/@danieli-automation/create-devops-plugin/src/template/files/tsconfigJson.ts +30 -0
  16. package/@danieli-automation/create-devops-plugin/src/template/files/vitestConfig.ts +57 -0
  17. package/@danieli-automation/create-devops-plugin/src/template/files/webpackAppConfig.ts +31 -0
  18. package/@danieli-automation/create-devops-plugin/src/template/files/webpackCommonConfig.ts +116 -0
  19. package/@danieli-automation/create-devops-plugin/src/template/files/webpackConfig.ts +15 -0
  20. package/@danieli-automation/create-devops-plugin/src/template/files.ts +1 -0
  21. package/@danieli-automation/create-devops-plugin/src/template/fs.ts +85 -0
  22. package/@danieli-automation/create-devops-plugin/src/template/manifest.ts +40 -0
  23. package/@danieli-automation/create-devops-plugin/src/template/npm.ts +22 -0
  24. package/@danieli-automation/create-devops-plugin/src/template/static/fonts/AzDevMDL2.woff +0 -0
  25. package/@danieli-automation/create-devops-plugin/src/template/static/fonts/bowtie.woff2 +0 -0
  26. package/@danieli-automation/create-devops-plugin/src/template/static/fonts/fabric-icons.woff +0 -0
  27. package/@danieli-automation/create-devops-plugin/src/template/static/fonts/fluent-filled-v1.1.293.woff2 +0 -0
  28. package/@danieli-automation/create-devops-plugin/src/template/static/fonts/fluent-regular-v1.1.293.woff2 +0 -0
  29. package/@danieli-automation/create-devops-plugin/src/template/static/images/DigiMetLogo.jpeg +0 -0
  30. package/@danieli-automation/create-devops-plugin/src/template/static/images/danieliAutomation.png +0 -0
  31. package/@danieli-automation/create-devops-plugin/src/template/static/images/danieliAutomationBlack.jpg +0 -0
  32. package/@danieli-automation/create-devops-plugin/src/template/static/images/danieli_digi_met_logo.jpeg +0 -0
  33. package/@danieli-automation/create-devops-plugin/src/template/static/images/logoSmallpng.png +0 -0
  34. package/@danieli-automation/create-devops-plugin/src/template/types.ts +14 -0
  35. package/@danieli-automation/create-devops-plugin/src/template/utils.ts +22 -0
  36. package/@danieli-automation/create-devops-plugin/tsconfig.json +8 -0
  37. package/@danieli-automation/devops-plugin-core/package.json +27 -0
  38. package/@danieli-automation/devops-plugin-core/src/core/azureClients.ts +18 -0
  39. package/@danieli-automation/devops-plugin-core/src/core/storage/createStore.ts +65 -0
  40. package/@danieli-automation/devops-plugin-core/src/core/storage/hooks/useCrossTeamSprintInstance.ts +145 -0
  41. package/@danieli-automation/devops-plugin-core/src/core/storage/hooks/useTaskOrder.ts +125 -0
  42. package/@danieli-automation/devops-plugin-core/src/core/storage/hooks/useWorkItemOrder.ts +86 -0
  43. package/@danieli-automation/devops-plugin-core/src/core/storage/index.ts +13 -0
  44. package/@danieli-automation/devops-plugin-core/src/core/storage/keys.ts +31 -0
  45. package/@danieli-automation/devops-plugin-core/src/core/storage/repositories/instance.ts +184 -0
  46. package/@danieli-automation/devops-plugin-core/src/core/storage/repositories/taskOrder.ts +59 -0
  47. package/@danieli-automation/devops-plugin-core/src/core/storage/repositories/workItemOrder.ts +60 -0
  48. package/@danieli-automation/devops-plugin-core/src/core/storage/stores.ts +18 -0
  49. package/@danieli-automation/devops-plugin-core/src/core/types/AdoWorkItemType.ts +18 -0
  50. package/@danieli-automation/devops-plugin-core/src/core/types/KVStoreType.ts +1 -0
  51. package/@danieli-automation/devops-plugin-core/src/core/types/ScopeType.ts +1 -0
  52. package/@danieli-automation/devops-plugin-core/src/core/types/SelectedProjectType.ts +8 -0
  53. package/@danieli-automation/devops-plugin-core/src/core/types/SortConfigType.ts +2 -0
  54. package/@danieli-automation/devops-plugin-core/src/core/types/instance/CreateInstanceInputType.ts +10 -0
  55. package/@danieli-automation/devops-plugin-core/src/core/types/instance/CrossSprintInstanceType.ts +20 -0
  56. package/@danieli-automation/devops-plugin-core/src/core/types/instance/DefaultInstanceType.ts +12 -0
  57. package/@danieli-automation/devops-plugin-core/src/core/types/instance/InstanceRowType.ts +18 -0
  58. package/@danieli-automation/devops-plugin-core/src/core/types/instance/UpdateInstanceInputType.ts +10 -0
  59. package/@danieli-automation/devops-plugin-core/src/core/types/taskOrder/TaskOrderMapType.ts +3 -0
  60. package/@danieli-automation/devops-plugin-core/src/core/types/taskOrder/TaskOrderType.ts +1 -0
  61. package/@danieli-automation/devops-plugin-core/src/core/types/workItemOrder/WorkItemOrderMapType.ts +3 -0
  62. package/@danieli-automation/devops-plugin-core/src/core/types/workItemOrder/WorkItemOrderType.ts +1 -0
  63. package/@danieli-automation/devops-plugin-core/src/index.ts +1 -0
  64. package/@danieli-automation/devops-plugin-core/src/pluginCore.ts +12 -0
  65. package/@danieli-automation/devops-plugin-core/tsconfig.json +16 -0
  66. package/@danieli-automation/devops-plugin-features/package.json +31 -0
  67. package/@danieli-automation/devops-plugin-features/src/app/stores/useUIStore.ts +12 -0
  68. package/@danieli-automation/devops-plugin-features/src/app/utils/date.ts +9 -0
  69. package/@danieli-automation/devops-plugin-features/src/app/utils/global.ts +9 -0
  70. package/@danieli-automation/devops-plugin-features/src/core/azureClients.ts +12 -0
  71. package/@danieli-automation/devops-plugin-features/src/features/instances/constants/InstanceConstant.ts +5 -0
  72. package/@danieli-automation/devops-plugin-features/src/features/instances/hooks/useInstancePermission.ts +127 -0
  73. package/@danieli-automation/devops-plugin-features/src/features/instances/stores/__tests__/useInstanceStore.test.ts +25 -0
  74. package/@danieli-automation/devops-plugin-features/src/features/instances/stores/types/InstanceStoreType.ts +7 -0
  75. package/@danieli-automation/devops-plugin-features/src/features/instances/stores/useInstanceStore.ts +9 -0
  76. package/@danieli-automation/devops-plugin-features/src/features/instances/types/CrossSprintInstanceType.ts +20 -0
  77. package/@danieli-automation/devops-plugin-features/src/features/instances/utils/instance.ts +55 -0
  78. package/@danieli-automation/devops-plugin-features/src/features/iterations/api/iterations.ts +298 -0
  79. package/@danieli-automation/devops-plugin-features/src/features/iterations/services/IterationService.ts +215 -0
  80. package/@danieli-automation/devops-plugin-features/src/features/iterations/types/IterationInfoType.ts +15 -0
  81. package/@danieli-automation/devops-plugin-features/src/features/iterations/utils/__tests__/iteration.test.ts +39 -0
  82. package/@danieli-automation/devops-plugin-features/src/features/iterations/utils/iteration.ts +132 -0
  83. package/@danieli-automation/devops-plugin-features/src/features/teams/api/projects.ts +79 -0
  84. package/@danieli-automation/devops-plugin-features/src/features/teams/api/teams.ts +29 -0
  85. package/@danieli-automation/devops-plugin-features/src/features/teams/api/users.ts +124 -0
  86. package/@danieli-automation/devops-plugin-features/src/features/teams/services/ProjectService.ts +80 -0
  87. package/@danieli-automation/devops-plugin-features/src/features/teams/types/SelectedProjectType.ts +8 -0
  88. package/@danieli-automation/devops-plugin-features/src/features/teams/types/TeamMemberType.ts +7 -0
  89. package/@danieli-automation/devops-plugin-features/src/features/teams/types/TeamRowSeedType.ts +6 -0
  90. package/@danieli-automation/devops-plugin-features/src/features/teams/types/UserSearchResultType.ts +8 -0
  91. package/@danieli-automation/devops-plugin-features/src/features/work-items/api/states.ts +24 -0
  92. package/@danieli-automation/devops-plugin-features/src/features/work-items/api/wiql.ts +146 -0
  93. package/@danieli-automation/devops-plugin-features/src/features/work-items/api/workItems.ts +193 -0
  94. package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/DefaultConsant.ts +43 -0
  95. package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/FieldConstant.ts +27 -0
  96. package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/StateConstant.ts +16 -0
  97. package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/TypeConstant.ts +8 -0
  98. package/@danieli-automation/devops-plugin-features/src/features/work-items/services/WIQLService.ts +101 -0
  99. package/@danieli-automation/devops-plugin-features/src/features/work-items/services/WorkItemService.ts +54 -0
  100. package/@danieli-automation/devops-plugin-features/src/features/work-items/types/AdoWorkItemType.ts +18 -0
  101. package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/__tests__/filter.test.ts +87 -0
  102. package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/__tests__/workItem.test.ts +116 -0
  103. package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/filter.ts +189 -0
  104. package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/workItem.ts +245 -0
  105. package/@danieli-automation/devops-plugin-features/src/index.ts +0 -0
  106. package/@danieli-automation/devops-plugin-features/src/test/mocks/azure-devops-extension-api/Work.ts +5 -0
  107. package/@danieli-automation/devops-plugin-features/tsconfig.json +18 -0
  108. package/@danieli-automation/devops-plugin-features/vitest.config.ts +21 -0
  109. package/README.md +55 -0
  110. package/devops-plugin-kit-0.1.0.tgz +0 -0
  111. package/package.json +29 -0
  112. package/tsconfig.base.json +15 -0
@@ -0,0 +1,193 @@
1
+ import { WorkItem, WorkItemErrorPolicy, WorkItemExpand } from "azure-devops-extension-api/WorkItemTracking";
2
+ import { witClient } from "core/azureClients";
3
+ import { AdoWorkItemType } from 'features/work-items/types/AdoWorkItemType';
4
+
5
+ /**
6
+ * Fetches work items by their IDs within a project.
7
+ *
8
+ * @param ids - Array of work item IDs to fetch
9
+ * @param projectId - Optional project ID to scope the query
10
+ * @param fields - Optional array of fields to retrieve for each work item
11
+ * @param expand - Optional expansion options for the work items
12
+ * @returns Promise resolving to an array of work items
13
+ */
14
+ export async function fetchWorkItemsByIds(ids: number[], projectId?: string, fields?: string[], expand: WorkItemExpand = WorkItemExpand.All): Promise<AdoWorkItemType[]> {
15
+ if (!ids || ids.length === 0) return [];
16
+
17
+ const client = witClient();
18
+ const uniqueIds = Array.from(new Set(ids));
19
+ const BATCH_SIZE = 150;
20
+ const result: AdoWorkItemType[] = [];
21
+
22
+ const fieldsArg = expand !== WorkItemExpand.None ? undefined : fields;
23
+
24
+ for (let i = 0; i < uniqueIds.length; i += BATCH_SIZE) {
25
+ const slice = uniqueIds.slice(i, i + BATCH_SIZE);
26
+
27
+ try {
28
+ const batch = await client.getWorkItems(
29
+ slice,
30
+ projectId,
31
+ fieldsArg,
32
+ undefined,
33
+ expand,
34
+ WorkItemErrorPolicy.Omit
35
+ );
36
+
37
+ if (Array.isArray(batch)) {
38
+ const cleaned = batch.filter((wi): wi is WorkItem => !!wi && typeof wi.id === "number" && !!wi.fields)
39
+ result.push(...cleaned);
40
+ }
41
+
42
+ } catch (err) {
43
+ console.error("[fetchWorkItemsByIds] failed batch", { projectId, slice }, err);
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+
49
+ /**
50
+ * Fetches work item details by given id and project id
51
+ *
52
+ * @param id - the id of the work item
53
+ * @param project - the id of the related project
54
+ * @param fields - the work items to select
55
+ * @returns Promise resolving to the object of work item
56
+ */
57
+ export async function fetchWorkItemById(id: number, project?: string, fields?: string[], expand: WorkItemExpand = WorkItemExpand.Relations) {
58
+ const wit = witClient();
59
+ const fieldsArg = expand !== WorkItemExpand.None ? undefined : fields;
60
+ const workItem = await wit.getWorkItem(id, project, fieldsArg, undefined, expand);
61
+ return workItem;
62
+ }
63
+
64
+ /**
65
+ * Fetches child work items for a given parent work item within a project.
66
+ *
67
+ * @param projectId - The ID of the project
68
+ * @param parentId - The ID of the parent work item
69
+ * @param fields - Optional array of fields to retrieve for each child work item
70
+ * @param signal - Optional AbortSignal to cancel the request
71
+ * @returns Promise resolving to an array of child work items
72
+ *
73
+ */
74
+ export async function fetchChildren(projectId: string, parentId: number, fields?: string[], signal?: AbortSignal) {
75
+
76
+ if (signal?.aborted) return [];
77
+
78
+ const wit = witClient();
79
+ const parent = await wit.getWorkItem(parentId, projectId, undefined, undefined, WorkItemExpand.Relations);
80
+ const rels = parent.relations || [];
81
+ const childIdArray: number[] = [];
82
+
83
+ for (const r of rels) {
84
+ if (signal?.aborted) return [];
85
+ if (r.rel === "System.LinkTypes.Hierarchy-Forward") {
86
+ const m = r.url?.match(/(\d+)$/);
87
+ if (m) childIdArray.push(+m[1]);
88
+ }
89
+ }
90
+
91
+ if (childIdArray.length === 0) return [];
92
+
93
+ return fetchWorkItemsByIds(childIdArray, projectId, fields);
94
+ }
95
+
96
+ /**
97
+ * Updates a work item with the specified changes within a project.
98
+ *
99
+ * @param taskId - The ID of the work item to update
100
+ * @param changes - An array of change operations to apply to the work item
101
+ * @param project - The project ID where the work item resides
102
+ * @returns Promise resolving to the updated work item
103
+ *
104
+ */
105
+ export async function updateWorkItem(taskId: number, changes: any[], project: string) {
106
+
107
+ const wit = witClient();
108
+ const wi = await wit.getWorkItem(taskId, project, undefined, undefined, WorkItemExpand.Fields);
109
+ const currentRev = wi.rev;
110
+ return wit.updateWorkItem(
111
+ [
112
+ { op: "test", path: "/rev", value: currentRev },
113
+ ...changes
114
+ ],
115
+ taskId,
116
+ project,
117
+ false,
118
+ false
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Creates a new work item within a project, optionally linking it to a parent work item.
124
+ *
125
+ * @param project - The project ID where the work item will be created
126
+ * @param parentId - The ID of the parent work item, if any
127
+ * @param fields - A record of fields to set on the new work item
128
+ * @returns Promise resolving to the created work item
129
+ *
130
+ */
131
+ export async function createWorkItem(project: string, parentId: number, fields: Record<any, any>) {
132
+ const wit = witClient();
133
+ const { title, state, iterationPath, areaPath, workItemType, parent } = fields;
134
+
135
+ const patchDocument = [
136
+ { op: "add", path: "/fields/System.Title", value: title ?? "New Task" },
137
+ { op: "add", path: "/fields/System.State", value: state ?? "To Do" },
138
+ { op: "add", path: "/fields/System.AreaPath", value: areaPath },
139
+ { op: "add", path: "/fields/System.IterationPath", value: normalizeAndEscapeIterationPath(iterationPath) },
140
+ { op: "add", path: "/fields/System.Parent", value: parent },
141
+ //{ op: "add", path: "/fields/System.History", value: }
142
+ ];
143
+
144
+ if (parentId) {
145
+ patchDocument.push({
146
+ op: "add",
147
+ path: "/relations/-",
148
+ value: {
149
+ rel: "System.LinkTypes.Hierarchy-Reverse",
150
+ url: `vstfs:///WorkItemTracking/WorkItem/${parentId}`,
151
+ },
152
+ });
153
+ }
154
+
155
+ const result = await wit.createWorkItem(patchDocument, project, workItemType);
156
+
157
+ return result;
158
+ }
159
+
160
+ /**
161
+ * Deletes a work item within a project.
162
+ *
163
+ * @param taskId - The ID of the work item to delete
164
+ * @param project - The project ID where the work item resides
165
+ * @returns Promise resolving to the deletion result
166
+ *
167
+ */
168
+ export async function deleteWorkItem(taskId: number, project: string) {
169
+ const wit = witClient();
170
+ return wit.deleteWorkItem(taskId, project);
171
+ }
172
+
173
+ //#region Private Functions
174
+
175
+ /**
176
+ * Normalize then escape backslashes so result is safe for Azure DevOps JSON payloads.
177
+ * This is idempotent: calling it multiple times yields the same result.
178
+ *
179
+ * Examples at runtime (string contents):
180
+ * "JOB_CLIENTE1\\Sprint 5" -> "JOB_CLIENTE1\\\\Sprint 5"
181
+ * "JOB_CLIENTE1\Sprint 5" -> "JOB_CLIENTE1\\\\Sprint 5"
182
+ *
183
+ * @param path Iteration path value.
184
+ * @returns Normalized and escaped iteration path.
185
+ */
186
+ function normalizeAndEscapeIterationPath(path: string) {
187
+ if (path == null) return path; // keep null/undefined as-is
188
+ // Collapse any run of backslashes to a single backslash
189
+ const collapsed = path.replace(/\\+/g, '\\');
190
+ // Escape single backslashes for JSON payload (double them)
191
+ return collapsed.replace(/\\/g, '\\\\');
192
+ }
193
+ //#endregion
@@ -0,0 +1,43 @@
1
+ import StateConstant from "./StateConstant.js";
2
+ import TypeConstant from "./TypeConstant.js";
3
+
4
+ export const WORK_ITEM_TYPES = [TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE, TypeConstant.BUG_TYPE, TypeConstant.TASK_TYPE];
5
+
6
+ //@TODO: States should be fetched from the api
7
+ export const WORK_ITEM_STATES = [
8
+ StateConstant.NEW_STATE,
9
+ StateConstant.TO_DO_STATE,
10
+ StateConstant.IN_PROGRESS_STATE,
11
+ StateConstant.DONE_STATE,
12
+ StateConstant.EXECUTING_STATE,
13
+ StateConstant.TERMINATED_STATE,
14
+ StateConstant.APPROVED_STATE,
15
+ StateConstant.REMOVED_STATE,
16
+ StateConstant.COMMITTED_STATE,
17
+ StateConstant.DEFAULT_STATE
18
+ ];
19
+
20
+ //@TODO: Colors should be fetched from the api
21
+ export const WORK_ITEM_STATE_COLORS: { [state: string]: string } = {
22
+ [StateConstant.NEW_STATE]: "rgb(178, 178, 178)",
23
+ [StateConstant.COMMITTED_STATE]: "#007acc",
24
+ [StateConstant.IN_PROGRESS_STATE]: "#007acc",
25
+ [StateConstant.EXECUTING_STATE]: "#A020F0",
26
+ [StateConstant.TERMINATED_STATE]: "#6BB700",
27
+ [StateConstant.APPROVED_STATE]: "rgb(178, 178, 178)",
28
+ [StateConstant.REMOVED_STATE]: "rgba(218, 213, 208, 0.5)",
29
+ [StateConstant.TO_DO_STATE]: "rgb(178, 178, 178)",
30
+ [StateConstant.DONE_STATE]: "#339933",
31
+ [StateConstant.DEFAULT_STATE]: "rgb(178, 178, 178)",
32
+ [StateConstant.BLOCKED_STATE]: "rgb(230, 0, 23);"
33
+ };
34
+
35
+ //@TODO: Icons should be fetched from the api
36
+ export const WORK_ITEM_TYPE_ICON: { [type: string]: { iconName: string; color: string } } = {
37
+ [TypeConstant.EPIC_TYPE]: { iconName: "CrownSolid", color: "rgb(224,108,0)" },
38
+ [TypeConstant.FEATURE_TYPE]: { iconName: "Trophy", color: "rgb(119,59,147)" },
39
+ [TypeConstant.BUG_TYPE]: { iconName: "LadybugSolid", color: "rgb(204, 41, 6)" },
40
+ [TypeConstant.TASK_TYPE]: { iconName: "TaskSolid", color: "rgb(164, 136, 10)" },
41
+ [TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE]: { iconName: "PageListSolid", color: "rgb(0, 152, 199)" },
42
+ "Default": { iconName: "WorkItem", color: "#605E5C" }
43
+ };
@@ -0,0 +1,27 @@
1
+ //Work Item Field Constants
2
+
3
+ export default {
4
+ WORK_ITEM_FIELD_ID: "System.Id",
5
+ WORK_ITEM_FIELD_ASSIGNED_TO: "System.AssignedTo",
6
+ WORK_ITEM_FIELD_STATE: "System.State",
7
+ WORK_ITEM_FIELD_WORK_ITEM_TYPE: "System.WorkItemType",
8
+ WORK_ITEM_FIELD_PARENT: "System.Parent",
9
+ WORK_ITEM_FIELD_TITLE: "System.Title",
10
+ WORK_ITEM_FIELD_CREATED_DATE: "System.CreatedDate",
11
+ WORK_ITEM_FIELD_CHANGED_DATE: "System.ChangedDate",
12
+ WORK_ITEM_FIELD_AREA_PATH: "System.AreaPath",
13
+ WORK_ITEM_FIELD_ITERATION_LEVEL_1: "System.IterationLevel1",
14
+ WORK_ITEM_FIELD_ITERATION_LEVEL_2: "System.IterationLevel2",
15
+ WORK_ITEM_FIELD_ITERATION_PATH: "System.IterationPath",
16
+ WORK_ITEM_FIELD_ITERATION_ID: "System.IterationId",
17
+ WORK_ITEM_FIELD_TEAM_PROJECT: "System.TeamProject",
18
+ WORK_ITEM_FIELD_TAGS: "System.Tags",
19
+ WORK_ITEM_FIELD_CHANGED_BY: "System.ChangedBy",
20
+ WORK_ITEM_FIELD_EFFORT: "Microsoft.VSTS.Scheduling.Effort",
21
+ WORK_ITEM_FIELD_REMAINING_WORK: "Microsoft.VSTS.Scheduling.RemainingWork",
22
+ WORK_ITEM_FIELD_CLOSED_DATE: "Microsoft.VSTS.Common.ClosedDate",
23
+ WORK_ITEM_FIELD_PRIORITY: "Microsoft.VSTS.Common.Priority",
24
+ WORK_ITEM_FIELD_TIME_SPENT: "Custom.Timespent",
25
+ WORK_ITEM_FIELD_ITERATION_INFO: "Custom.IterationInfo",
26
+ WORK_ITEM_FIELD_ORDER: "order"
27
+ }
@@ -0,0 +1,16 @@
1
+ export default {
2
+ NEW_STATE: "New",
3
+ COMMITTED_STATE: "Committed",
4
+ IN_PROGRESS_STATE: "In Progress",
5
+ EXECUTING_STATE: "Executing",
6
+ TERMINATED_STATE: "Terminated",
7
+ APPROVED_STATE: "Approved",
8
+ REMOVED_STATE: "Removed",
9
+ TO_DO_STATE: "To Do",
10
+ DONE_STATE: "Done",
11
+ DEFAULT_STATE: "Default",
12
+ COMPLETED_STATE: "Completed",
13
+ CLOSED_STATE: "Closed",
14
+ RESOLVED_STATE: "Resolved",
15
+ BLOCKED_STATE: "Blocked"
16
+ }
@@ -0,0 +1,8 @@
1
+ //Work Item Type Constants
2
+ export default {
3
+ EPIC_TYPE: "Epic",
4
+ FEATURE_TYPE: "Feature",
5
+ PRODUCT_BACKLOG_ITEM_TYPE: "Product Backlog Item",
6
+ BUG_TYPE: "Bug",
7
+ TASK_TYPE: "Task"
8
+ }
@@ -0,0 +1,101 @@
1
+ import { witClient } from "core/azureClients";
2
+ import { AreaGroup } from "features/teams/services/ProjectService";
3
+ import { wiqlForMultipleIterations, wiqlForParentWorkItems, wiqlForSingleIteration } from "features/work-items/api/wiql";
4
+ import { fetchWorkItemsByIds } from "features/work-items/api/workItems";
5
+ import { AdoWorkItemType } from "features/work-items/types/AdoWorkItemType";
6
+
7
+ export const WIQLService = {
8
+
9
+ /**
10
+ * Fetches parent work items (PBIs and Bugs) for the given child work item IDs across projects.
11
+ *
12
+ * @param workItemIds
13
+ * @param projectId
14
+ * @returns Promise resolving to an array of parent work items
15
+ *
16
+ */
17
+ getParentWorkItemsForPbisAndBugs: async (workItemIds: number[], projectId?: string): Promise<AdoWorkItemType[]> => {
18
+
19
+ const wiql = wiqlForParentWorkItems(workItemIds);
20
+ if (!wiql) return [];
21
+
22
+ const wit = witClient();
23
+ const res = await wit.queryByWiql({ query: wiql }, undefined, projectId ?? "");
24
+
25
+ const parentIds = new Set<number>();
26
+ const relations = res.workItemRelations || [];
27
+
28
+ for (const rel of relations) {
29
+ if (rel.source?.id) parentIds.add(rel.source.id);
30
+ }
31
+
32
+ if (parentIds.size === 0) return [];
33
+ const parentIdArray = Array.from(parentIds);
34
+ return fetchWorkItemsByIds(parentIdArray, projectId ?? "");
35
+ },
36
+
37
+ /**
38
+ * @description: Fetches Task IDs that belong to the selected iteration, per project area groups.
39
+ * @param areaGroups Project+area scoping.
40
+ * @param iterationPath Selected iteration path.
41
+ * @returns Promise resolving to a unique list of task IDs.
42
+ */
43
+ getTaskIdsForSelectedIteration: async (areaGroups: AreaGroup[], iterationPaths: string[]): Promise<number[]> => {
44
+ const out = new Set<number>();
45
+ const wit = witClient();
46
+
47
+ const results = await Promise.all(
48
+ areaGroups.map(async (g) => {
49
+ if (!g.areaPaths?.length) return [];
50
+
51
+ const { tasksQuery } = iterationPaths.length > 1
52
+ ? wiqlForMultipleIterations(iterationPaths, g.areaPaths)
53
+ : wiqlForSingleIteration(iterationPaths[0], g.areaPaths);
54
+ const res = await wit.queryByWiql({ query: tasksQuery }, undefined, g.projectId ?? "");
55
+ return (res.workItems ?? []).map(w => w.id).filter(Boolean) as number[];
56
+ })
57
+ );
58
+
59
+ for (const ids of results) {
60
+ for (const id of ids) out.add(id);
61
+ }
62
+
63
+ return Array.from(out);
64
+ },
65
+
66
+ /**
67
+ * @description: Fetches parent work item IDs (PBIs/Bugs) that belong to the selected iteration,
68
+ * scoped by project area groups.
69
+ * @param areaGroups Project+area scoping.
70
+ * @param iterationPath Selected iteration path.
71
+ * @returns Promise resolving to a unique list of parent work item IDs.
72
+ */
73
+ getParentIdsForSelectedIteration: async (areaGroups: AreaGroup[], iterationPaths: string[]): Promise<number[]> => {
74
+ const out = new Set<number>();
75
+ const wit = witClient();
76
+
77
+ const results = await Promise.all(
78
+ areaGroups.map(async (g) => {
79
+ if (!g.areaPaths?.length) return [];
80
+
81
+ const { parentsQuery } = iterationPaths.length > 1
82
+ ? wiqlForMultipleIterations(iterationPaths, g.areaPaths)
83
+ : wiqlForSingleIteration(iterationPaths[0], g.areaPaths);
84
+ const res = await wit.queryByWiql({ query: parentsQuery }, undefined, g.projectId ?? "");
85
+ return (res.workItems ?? []).map(w => w.id).filter(Boolean) as number[];
86
+ })
87
+ );
88
+
89
+ for (const ids of results) {
90
+ for (const id of ids) out.add(id);
91
+ }
92
+
93
+ return Array.from(out);
94
+ }
95
+ };
96
+
97
+
98
+
99
+
100
+
101
+
@@ -0,0 +1,54 @@
1
+ import { WorkItemExpand } from "azure-devops-extension-api/WorkItemTracking";
2
+ import { fetchWorkItemById } from "features/work-items/api/workItems";
3
+ import FieldConstant from "features/work-items/constants/FieldConstant";
4
+
5
+ export const WorkItemService = {
6
+
7
+ /**
8
+ * Fetches a single work item including its relations (parent/child links, etc.).
9
+ * Used for traversing work item hierarchy via "System.LinkTypes.Hierarchy-Forward".
10
+ *
11
+ * @param id - Work item ID
12
+ * @param projectId - Project name or ID used by the client
13
+ * @returns Promise resolving to the work item including `relations`
14
+ */
15
+ collectDescendantWorkItemIds: async (rootId: number, project: string): Promise<number[]> => {
16
+ const visited = new Set<number>();
17
+ const result: number[] = [];
18
+
19
+ const stack: number[] = [rootId];
20
+
21
+ while (stack.length > 0) {
22
+ const id = stack.pop()!;
23
+ if (visited.has(id)) continue;
24
+ visited.add(id);
25
+
26
+ const wi = await fetchWorkItemById(id, project, [FieldConstant.WORK_ITEM_FIELD_ID]);
27
+ const relations = wi?.relations ?? [];
28
+
29
+ for (const r of relations) {
30
+ if (r.rel !== "System.LinkTypes.Hierarchy-Forward") continue;
31
+ const match = r.url?.match(/(\d+)$/);
32
+ if (!match) continue;
33
+ const childId = Number(match[1]);
34
+ if (!Number.isFinite(childId) || visited.has(childId)) continue;
35
+
36
+ result.push(childId);
37
+ stack.push(childId);
38
+ }
39
+ }
40
+ return result;
41
+ },
42
+
43
+ /**
44
+ * Fetches a single work item including its relations (parent/child links, etc.).
45
+ *
46
+ * @param id - Work item ID
47
+ * @param projectId - Project name or ID used by the client
48
+ * @returns Promise resolving to the work item including `relations`
49
+ */
50
+ getWorkItemById: async (workItemId: number, project?: string, fields?: string[], expand?: WorkItemExpand) => {
51
+ const workItem = await fetchWorkItemById(workItemId, project, fields, expand);
52
+ return workItem;
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ import { WorkItemRelation } from "azure-devops-extension-api/WorkItemTracking";
2
+
3
+ export type AdoWorkItemType = {
4
+ id: any,
5
+ tempId?: string
6
+ fields: { [key: string]: any; }
7
+ _links?: any;
8
+ url?: string;
9
+ order?: number;
10
+ displayOrder?: string;
11
+ rev?: number;
12
+ multiLineFields?: { [k: string]: string[] };
13
+ relations?: WorkItemRelation[];
14
+ isNewTask?: boolean;
15
+ parentWorkItem?: AdoWorkItemType;
16
+ isFaded?: boolean | undefined;
17
+ _children?: AdoWorkItemType[];
18
+ };
@@ -0,0 +1,87 @@
1
+ import FieldConstant from "features/work-items/constants/FieldConstant";
2
+ import StateConstant from "features/work-items/constants/StateConstant";
3
+ import TypeConstant from "features/work-items/constants/TypeConstant";
4
+ import { AdoWorkItemType } from "features/work-items/types/AdoWorkItemType";
5
+ import { describe, expect, it } from "vitest";
6
+ import { filterEpics, filterFeatures, filterPbisAndBugs, filterTasks } from '../filter.js';
7
+
8
+ describe("Utils: Filter Helper Tests", () => {
9
+
10
+ const epicTypeWorkItem: AdoWorkItemType = {
11
+ id: 1,
12
+ fields: {
13
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "1",
14
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 1",
15
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.EPIC_TYPE
16
+ }
17
+ }
18
+
19
+ const featureTypeWorkItem: AdoWorkItemType = {
20
+ id: 2,
21
+ fields: {
22
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "2",
23
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 2",
24
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.FEATURE_TYPE
25
+ }
26
+ }
27
+
28
+ const pbiTypeWorkItem: AdoWorkItemType = {
29
+ id: 3,
30
+ fields: {
31
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "3",
32
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 3",
33
+ [FieldConstant.WORK_ITEM_FIELD_STATE]: StateConstant.EXECUTING_STATE,
34
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE
35
+ }
36
+ }
37
+
38
+ const bugTypeWorkItem: AdoWorkItemType = {
39
+ id: 4,
40
+ fields: {
41
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "4",
42
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 4",
43
+ [FieldConstant.WORK_ITEM_FIELD_STATE]: StateConstant.EXECUTING_STATE,
44
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.BUG_TYPE
45
+ }
46
+ }
47
+
48
+ const taskTypeWorkItem: AdoWorkItemType = {
49
+ id: 5,
50
+ fields: {
51
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "5",
52
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 5",
53
+ [FieldConstant.WORK_ITEM_FIELD_STATE]: StateConstant.IN_PROGRESS_STATE,
54
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.TASK_TYPE
55
+ }
56
+ }
57
+
58
+ const workItems: AdoWorkItemType[] = [epicTypeWorkItem, featureTypeWorkItem, pbiTypeWorkItem, bugTypeWorkItem, taskTypeWorkItem];
59
+
60
+ //#region filterEpics
61
+ it("filterEpics: returns filtered epic type work items from given array of data", () => {
62
+ expect(filterEpics(workItems)).toEqual([{ ...epicTypeWorkItem }]);
63
+ });
64
+ //#endregion
65
+
66
+ //#region filterFeatures
67
+ it("filterFeatures: returns filtered feature type work items from given array of data", () => {
68
+ expect(filterFeatures(workItems)).toEqual([{ ...featureTypeWorkItem }]);
69
+ });
70
+ //#endregion
71
+
72
+ //#region filterPbisAndBugs
73
+ it("filterPbisAndBugs: returns filtered pbis and bug type work items from given array of data", () => {
74
+ const result = filterPbisAndBugs(workItems);
75
+
76
+ expect(result).toHaveLength(2);
77
+ expect(result).toEqual(expect.arrayContaining([bugTypeWorkItem, pbiTypeWorkItem]));
78
+ });
79
+ //#endregions
80
+
81
+ //#region filterTasks
82
+ it("filterTasks: returns filtered task type work items from given array of data", () => {
83
+ expect(filterTasks(workItems)).toEqual([{ ...taskTypeWorkItem }]);
84
+ });
85
+ //#endregions
86
+
87
+ });
@@ -0,0 +1,116 @@
1
+ import FieldConstant from "features/work-items/constants/FieldConstant";
2
+ import TypeConstant from "features/work-items/constants/TypeConstant";
3
+ import { describe, expect, it } from "vitest";
4
+ import { AdoWorkItemType } from "../../types/AdoWorkItemType.js";
5
+ import { calculateTotalRemainingWorkByWorkItems, getEpicByWorkItem, getFeatureByWorkItem } from "../workItem.js";
6
+
7
+ describe("Utils: WorkItem Helper Tests", () => {
8
+
9
+ const epic7420: AdoWorkItemType = {
10
+ id: 7420,
11
+ fields: {
12
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "7420",
13
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 7420",
14
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.EPIC_TYPE,
15
+ },
16
+ relations: [
17
+ {
18
+ rel: "System.LinkTypes.Hierarchy-Forward",
19
+ url: "https://dev.azure.com/org/project/_apis/wit/workItems/7428", // child = feature 7428
20
+ attributes: {}
21
+ },
22
+ ],
23
+ };
24
+
25
+ const epic7421: AdoWorkItemType = {
26
+ id: 7421,
27
+ fields: {
28
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "7421",
29
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 7421",
30
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.EPIC_TYPE,
31
+ },
32
+ relations: [],
33
+ };
34
+
35
+ const feature7428: AdoWorkItemType = {
36
+ id: 7428,
37
+ fields: {
38
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "7428",
39
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 7428",
40
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.FEATURE_TYPE,
41
+ },
42
+ relations: [
43
+ {
44
+ rel: "System.LinkTypes.Hierarchy-Forward",
45
+ url: "https://dev.azure.com/org/project/_apis/wit/workItems/2", // child = PBI id 2
46
+ attributes: {}
47
+ },
48
+ ],
49
+ };
50
+
51
+ const pbi2: AdoWorkItemType = {
52
+ id: 2,
53
+ fields: {
54
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "2",
55
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 2",
56
+ [FieldConstant.WORK_ITEM_FIELD_REMAINING_WORK]: 3,
57
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE,
58
+ },
59
+ };
60
+
61
+ const pbi3: AdoWorkItemType = {
62
+ id: 3,
63
+ fields: {
64
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "3",
65
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 3",
66
+ [FieldConstant.WORK_ITEM_FIELD_REMAINING_WORK]: 3,
67
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE,
68
+ },
69
+ };
70
+
71
+ const pbi4: AdoWorkItemType = {
72
+ id: 4,
73
+ fields: {
74
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "4",
75
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 4",
76
+ [FieldConstant.WORK_ITEM_FIELD_REMAINING_WORK]: 3,
77
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE,
78
+ },
79
+ };
80
+
81
+ const pbi5: AdoWorkItemType = {
82
+ id: 5,
83
+ fields: {
84
+ [FieldConstant.WORK_ITEM_FIELD_ID]: "5",
85
+ [FieldConstant.WORK_ITEM_FIELD_TITLE]: "This is test work item 5",
86
+ [FieldConstant.WORK_ITEM_FIELD_REMAINING_WORK]: 3,
87
+ [FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]: TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE,
88
+ },
89
+ };
90
+
91
+ const workItems: AdoWorkItemType[] = [epic7420, epic7421, feature7428, pbi2, pbi3, pbi4, pbi5];
92
+
93
+ //#region getEpicById
94
+ it("getEpicById: returns epic type work item by the id of child work item among the provided array of work items", () => {
95
+ const result = getEpicByWorkItem(pbi2, workItems);
96
+
97
+ expect(result).toEqual(epic7420);
98
+ });
99
+ //#endregion
100
+
101
+ //#region getFeatureById
102
+ it("getFeatureById: returns feature type work item by the id of child work item among the provided array of work items", () => {
103
+ const result = getFeatureByWorkItem(pbi2, workItems);
104
+
105
+ expect(result).toEqual(feature7428);
106
+ });
107
+ //#endregion
108
+
109
+ //#region calculateTotalRemainingWorkByWorkItems
110
+ it("calculateTotalRemainingWorkByWorkItems: returns total remaining work hours of the work items given", () => {
111
+ const result = calculateTotalRemainingWorkByWorkItems(workItems);
112
+
113
+ expect(result).toEqual(12);
114
+ });
115
+ //#endregion
116
+ });