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,189 @@
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
+
6
+ /**
7
+ * Filters work items to include only Epics.
8
+ *
9
+ * @param workItems Array of work items to filter.
10
+ * @returns Filtered array containing only Epics.
11
+ *
12
+ */
13
+ export function filterEpics(workItems: AdoWorkItemType[]) {
14
+ return workItems.filter(
15
+ wi => wi.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE] === TypeConstant.EPIC_TYPE
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Filters work items to include only Features.
21
+ *
22
+ * @param workItems Array of work items to filter.
23
+ * @returns Filtered array containing only Features.
24
+ */
25
+ export function filterFeatures(workItems: AdoWorkItemType[]) {
26
+ return workItems.filter(
27
+ wi => wi.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE] === TypeConstant.FEATURE_TYPE
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Filters work items to include only Product Backlog Items (PBIs) and Bugs.
33
+ *
34
+ * @param workItems Array of work items to filter.
35
+ * @returns Filtered array containing only PBIs and Bugs.
36
+ *
37
+ */
38
+ export function filterPbisAndBugs(workItems: AdoWorkItemType[]) {
39
+ return workItems.filter((workItem: AdoWorkItemType) => {
40
+ const workItemType = workItem.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE];
41
+ return workItemType === TypeConstant.PRODUCT_BACKLOG_ITEM_TYPE || workItemType === TypeConstant.BUG_TYPE;
42
+ });
43
+ };
44
+
45
+ /**
46
+ * Filters work items to include only Tasks that are not in the Removed state.
47
+ *
48
+ * @param workItems Array of work items to filter.
49
+ * @returns Filtered array containing only Tasks that are not removed.
50
+ *
51
+ */
52
+ export function filterTasks(workItems: AdoWorkItemType[]) {
53
+ return workItems.filter((workItem: AdoWorkItemType) => {
54
+ const workItemType = workItem.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE];
55
+ const isRemoved = workItem.fields[FieldConstant.WORK_ITEM_FIELD_STATE] === StateConstant.REMOVED_STATE;
56
+ return workItemType === TypeConstant.TASK_TYPE && !isRemoved;
57
+ });
58
+ };
59
+
60
+ /**
61
+ * @description: Filters child work items
62
+ * @param childrenWorkItems: Array of child work items to filter.
63
+ * @param selectedAssignedTo: An array of selected assignee unique names.
64
+ * @param parentId: The parent work item ID to filter by.
65
+ * @return: Filtered array of child work items.
66
+ */
67
+ export function filterChildrenWorkItemsByParentId(childrenWorkItems: AdoWorkItemType[], parentId: number, selectedAssignedTo?: string[]) {
68
+ return childrenWorkItems.filter((t: AdoWorkItemType) => {
69
+ if (!t) return false;
70
+
71
+ if (t.fields[FieldConstant.WORK_ITEM_FIELD_STATE] == StateConstant.REMOVED_STATE) return false;
72
+
73
+ const viaRelations = t.relations?.some(
74
+ (rel: any) => rel.rel === "System.LinkTypes.Hierarchy-Reverse" && rel.url.endsWith(`/${parentId}`)
75
+ );
76
+
77
+ const viaParentField = t.fields?.[FieldConstant.WORK_ITEM_FIELD_PARENT] === parentId;
78
+
79
+ if (!selectedAssignedTo || selectedAssignedTo.length === 0) {
80
+ return !!(viaRelations || viaParentField);
81
+ }
82
+
83
+ let isAssigneeMatched = filterWorkItemsByAssignee(t, selectedAssignedTo);
84
+
85
+ if (t._children && t._children.length > 0) {
86
+ for (const child of t._children) {
87
+ if (filterWorkItemsByAssignee(child, selectedAssignedTo)) {
88
+ isAssigneeMatched = true;
89
+ break;
90
+ }
91
+ }
92
+ if (!isAssigneeMatched) return false;
93
+ }
94
+
95
+ if (!isAssigneeMatched) return false;
96
+
97
+ return !!(viaRelations || viaParentField);
98
+ });
99
+ }
100
+
101
+ /**
102
+ * @description: Filters a work item based on selected assignees.
103
+ * @param workItem: The work item to filter.
104
+ * @param selectedAssignedTo: An array of selected assignee unique names.
105
+ * @return: True if the work item matches the selected assignees, false otherwise.
106
+ */
107
+ export function filterWorkItemsByAssignee(workItem: AdoWorkItemType, selectedAssignedTo: string[]) {
108
+ if (!selectedAssignedTo?.length) return true;
109
+
110
+ const assigned = workItem.fields?.[FieldConstant.WORK_ITEM_FIELD_ASSIGNED_TO] as any | undefined;
111
+ const uniqueName = assigned?.uniqueName as string | undefined;
112
+
113
+ const hasUnassigned = selectedAssignedTo.includes("__unassigned__");
114
+ const selectedReal = selectedAssignedTo.filter(x => x !== "__unassigned__");
115
+
116
+ const isUnassigned = !uniqueName;
117
+
118
+ const matchesReal = selectedReal.length > 0
119
+ ? (!!uniqueName && selectedReal.includes(uniqueName))
120
+ : false;
121
+
122
+ const matchesUnassigned = hasUnassigned && isUnassigned;
123
+
124
+ if (selectedReal.length === 0 && hasUnassigned) return matchesUnassigned;
125
+ if (selectedReal.length > 0 && !hasUnassigned) return matchesReal;
126
+ return matchesReal || matchesUnassigned;
127
+ }
128
+
129
+
130
+ /**
131
+ * @description: Checks if a work item matches a given keyword across defined fields.
132
+ * @param workItem The work item to check.
133
+ * @param keyword The keyword to match against the work item's specific fields.
134
+ * @returns True if the keyword matches any field in the work item that is specified before, false otherwise.
135
+ */
136
+ export function filterWorkItemsByKeyword(workItem: AdoWorkItemType, keyword: string): boolean {
137
+ const kw = keyword.trim().toLowerCase();
138
+ if (!kw) return true;
139
+
140
+ const fields = workItem.fields ?? {};
141
+
142
+ for (const field of [
143
+ FieldConstant.WORK_ITEM_FIELD_ID,
144
+ FieldConstant.WORK_ITEM_FIELD_TITLE,
145
+ FieldConstant.WORK_ITEM_FIELD_TAGS,
146
+ FieldConstant.WORK_ITEM_FIELD_AREA_PATH,
147
+ FieldConstant.WORK_ITEM_FIELD_ITERATION_PATH
148
+ ]) {
149
+ const value = fields[field];
150
+
151
+ if (!value) continue;
152
+
153
+ //for Id like fields
154
+ if (typeof value === "number") {
155
+ if (String(value) === kw) return true;
156
+ continue;
157
+ }
158
+
159
+ // for title like fields
160
+ if (typeof value === "string") {
161
+ if (value.toLowerCase().includes(kw)) return true;
162
+ continue;
163
+ }
164
+
165
+ //for area path and tags like fields
166
+ if (Array.isArray(value)) {
167
+ if (value.some(v => String(v).toLowerCase().includes(kw))) return true;
168
+ continue;
169
+ }
170
+ }
171
+
172
+ return false;
173
+ }
174
+
175
+ /**
176
+ * @description: Checks whether a work item contains at least one of the selected tags.
177
+ * Tags are read from the work item's tag field, split by semicolon (;),
178
+ * trimmed, and compared against the provided tag list.
179
+ * @param workItem The work item whose tags will be evaluated.
180
+ * @param selectedTags The list of tags to match against the work item's tags.
181
+ * @returns True if the work item contains at least one of the selected tags, false otherwise.
182
+ */
183
+ export function filterWorkItemsByTags(workItem: AdoWorkItemType, selectedTags: string[]) {
184
+ const tagsStr = String(workItem.fields[FieldConstant.WORK_ITEM_FIELD_TAGS] ?? "");
185
+ const tags = tagsStr.split(";").map(t => t.trim()).filter(Boolean);
186
+ const tagSet = new Set(tags);
187
+ const someTagsExists = selectedTags.some(t => tagSet.has(t));
188
+ return someTagsExists
189
+ }
@@ -0,0 +1,245 @@
1
+ import { TeamMember } from "features/teams/types/TeamMemberType";
2
+ import FieldConstant from "features/work-items/constants/FieldConstant";
3
+ import TypeConstant from "features/work-items/constants/TypeConstant";
4
+ import { AdoWorkItemType } from "features/work-items/types/AdoWorkItemType";
5
+ import { filterEpics, filterFeatures } from "./filter.js";
6
+
7
+ /**
8
+ * Get the Epic work item that is the ancestor of the given work item.
9
+ *
10
+ * @param workItem The work item for which to find the Epic ancestor.
11
+ * @param workItems The list of all work items to search within.
12
+ * @returns The Epic work item if found, otherwise undefined.
13
+ *
14
+ */
15
+ export function getEpicByWorkItem(workItem: AdoWorkItemType, workItems: AdoWorkItemType[]): AdoWorkItemType | undefined {
16
+ const epics = filterEpics(workItems);
17
+ const features = filterFeatures(workItems);
18
+
19
+ const hasChild = (parent: AdoWorkItemType, childId: number) => {
20
+ const relations = parent.relations || [];
21
+ return relations.some(rel => {
22
+ if (rel.rel !== "System.LinkTypes.Hierarchy-Forward") return false;
23
+ const match = rel.url && rel.url.match(/(\d+)$/);
24
+ const parsedId = match ? Number(match[1]) : null;
25
+ return parsedId === childId;
26
+ });
27
+ };
28
+
29
+ //find the feature that is (direct) parent of current item
30
+ let parentFeature: AdoWorkItemType | undefined;
31
+
32
+ if (workItem.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE] === TypeConstant.FEATURE_TYPE) {
33
+ parentFeature = workItem;
34
+ } else {
35
+ parentFeature = features.find(f => hasChild(f, workItem.id));
36
+ }
37
+
38
+ if (!parentFeature) {
39
+ return undefined;
40
+ }
41
+
42
+ return epics.find(e => hasChild(e, parentFeature!.id));
43
+ }
44
+
45
+ /**
46
+ * Builds a lookup map from Feature ID -> Epic metadata (id + title),
47
+ * by scanning Epic work items and their forward hierarchy child links.
48
+ *
49
+ * @param topLevelParentWorkItems A list of top-level parent work items (typically epics/features).
50
+ * @returns A map where the key is featureId and the value contains epicId and epicTitle.
51
+ *
52
+ */
53
+ export function getEpicByFeatureId(topLevelParentWorkItems: AdoWorkItemType[]) {
54
+ const map = new Map<number, { epicId: number; epicTitle: string }>();
55
+
56
+ for (const wi of topLevelParentWorkItems) {
57
+ const type = String(wi.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE] || "");
58
+ if (type.toLowerCase() !== "epic") continue;
59
+
60
+ const epicId = wi.id;
61
+ const epicTitle = String(wi.fields[FieldConstant.WORK_ITEM_FIELD_TITLE] || "");
62
+
63
+ for (const featureId of getChildWorkItemIdsFromRelations(wi)) {
64
+ map.set(featureId, { epicId, epicTitle });
65
+ }
66
+ }
67
+ return map;
68
+ }
69
+
70
+
71
+ /**
72
+ * Get the Feature work item that is the parent of the given work item.
73
+ *
74
+ * @param workItem The work item for which to find the Feature parent.
75
+ * @param workItems The list of all work items to search within.
76
+ * @returns The Feature work item if found, otherwise undefined.
77
+ *
78
+ */
79
+ export function getFeatureByWorkItem(workItem: AdoWorkItemType, workItems: AdoWorkItemType[]): AdoWorkItemType | undefined {
80
+ const features = filterFeatures(workItems);
81
+
82
+ return features?.find(f => {
83
+ const relations = f.relations || [];
84
+ return relations.some(rel => {
85
+ const childRel = rel.rel === "System.LinkTypes.Hierarchy-Forward";
86
+ const match = rel.url && rel.url.match(/(\d+)$/);
87
+ const childId = match ? Number(match[1]) : null;
88
+ return childRel && childId === workItem.id;
89
+ });
90
+ });
91
+ }
92
+
93
+
94
+ /**
95
+ * Builds a lookup map from Work Item ID -> Feature metadata (id + title),
96
+ * by scanning Feature work items and their forward hierarchy child links.
97
+ *
98
+ * @param topLevelParentWorkItems A list of top-level parent work items (typically features).
99
+ * @returns A map where the key is child workItemId and the value contains featureId and featureTitle.
100
+ *
101
+ */
102
+ export function getFeatureByWorkItemId(topLevelParentWorkItems: AdoWorkItemType[]) {
103
+ const map = new Map<number, { featureId: number; featureTitle: string }>();
104
+
105
+ for (const feature of topLevelParentWorkItems) {
106
+ const type = String(feature.fields[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE] || "");
107
+ if (type.toLowerCase() !== "feature") continue;
108
+
109
+ const featureId = feature.id;
110
+ const featureTitle = String(feature.fields[FieldConstant.WORK_ITEM_FIELD_TITLE] || "");
111
+
112
+ for (const childId of getChildWorkItemIdsFromRelations(feature)) {
113
+ map.set(childId, { featureId, featureTitle });
114
+ }
115
+ }
116
+
117
+ return map;
118
+ }
119
+
120
+ /**
121
+ * Calculates the total remaining work from an array of work items.
122
+ *
123
+ * @param workItems Array of work items to calculate remaining work from.
124
+ * @returns Total remaining work as a number.
125
+ *
126
+ */
127
+ export function calculateTotalRemainingWorkByWorkItems(workItems: AdoWorkItemType[]) {
128
+ return workItems.reduce((total, wi) => total + (Number(wi?.fields[FieldConstant.WORK_ITEM_FIELD_REMAINING_WORK]) || 0), 0);
129
+ }
130
+
131
+
132
+ /**
133
+ * Get the assignee from the given work item
134
+ *
135
+ * @param workItem The work item that is asked to get responsible assignee from
136
+ * @returns Assignee as Team Member type.
137
+ */
138
+ export function getAssigneeFromWorkItem(workItem: AdoWorkItemType) {
139
+ const v = workItem.fields?.[FieldConstant.WORK_ITEM_FIELD_ASSIGNED_TO];
140
+
141
+ if (!v || typeof v !== "object") {
142
+ return { id: "", displayName: "Unassigned", uniqueName: "" } as TeamMember;
143
+ }
144
+
145
+ return v as TeamMember;
146
+ };
147
+
148
+ /**
149
+ * Extracts child work item IDs from a work item's relations by reading
150
+ * forward hierarchy links (System.LinkTypes.Hierarchy-Forward).
151
+ *
152
+ * @param item The work item whose relations will be parsed for child links.
153
+ * @returns A list of child work item IDs. Returns an empty array if there are no relations.
154
+ *
155
+ */
156
+ export function getChildWorkItemIdsFromRelations(item: AdoWorkItemType): number[] {
157
+ if (!Array.isArray(item?.relations)) return [];
158
+
159
+ return item.relations
160
+ .filter((r: any) => r.rel === "System.LinkTypes.Hierarchy-Forward")
161
+ .map((r: any) => Number(r.url.match(/(\d+)$/)?.[1] ?? 0))
162
+ .filter((id: number) => Number.isFinite(id) && id > 0);
163
+ }
164
+
165
+ /**
166
+ * @description: Extracts parent work item IDs from task relations (Hierarchy-Reverse).
167
+ * @param tasks Array of task work items (must include relations).
168
+ * @returns Array of unique parent IDs.
169
+ */
170
+ export function extractParentIdsFromTasks(tasks: AdoWorkItemType[]): number[] {
171
+ const out = new Set<number>();
172
+
173
+ for (const t of tasks) {
174
+ for (const rel of t.relations ?? []) {
175
+ if (rel.rel !== "System.LinkTypes.Hierarchy-Reverse") continue;
176
+ const match = String(rel.url ?? "").match(/(\d+)$/);
177
+ if (!match) continue;
178
+ const id = Number(match[1]);
179
+ if (!Number.isNaN(id)) out.add(id);
180
+ }
181
+ }
182
+
183
+ return Array.from(out);
184
+ }
185
+
186
+ /**
187
+ * Checks whether a work item should be treated as a Task by comparing its
188
+ * work item type field against the Task type constant.
189
+ *
190
+ * @param item The work item to check.
191
+ * @returns True if the work item type matches Task, otherwise false.
192
+ *
193
+ */
194
+ export function isTask(item: AdoWorkItemType) {
195
+ return String(item?.fields?.[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE]).toLowerCase() === TypeConstant.TASK_TYPE.toLowerCase();
196
+ }
197
+
198
+ /**
199
+ * Parses and returns the tags of a work item as a cleaned string array.
200
+ * Azure DevOps stores tags as a semicolon-separated string.
201
+ *
202
+ * @param workItem The work item to extract tags from.
203
+ * @returns A trimmed, non-empty list of tags.
204
+ *
205
+ */
206
+ export function getTagsByWorkItem(workItem: AdoWorkItemType) {
207
+ const tags = (workItem.fields[FieldConstant.WORK_ITEM_FIELD_TAGS] || "").split(";");
208
+ const validatedTags = tags.map((tag: string) => tag.trim());
209
+ const filteredTags = validatedTags.filter((tag: string) => tag);
210
+ return filteredTags;
211
+ }
212
+
213
+ /**
214
+ * Returns the IDs of work items that can be expanded (i.e., non-task items that have at least one child).
215
+ * Typically used to determine which parent rows should show an expand/collapse control in the UI.
216
+ *
217
+ * Rules:
218
+ * - Excludes items of type "Task" (tasks are usually the leaf-level items).
219
+ * - Includes only items that have at least one child relation.
220
+ *
221
+ * @param workItems The list of work items to evaluate.
222
+ * @returns A list of work item IDs that are expandable.
223
+ */
224
+ export function getExpandableWorkItemIds(workItems: AdoWorkItemType[]) {
225
+ return workItems
226
+ .filter((w) => String(w.fields?.[FieldConstant.WORK_ITEM_FIELD_WORK_ITEM_TYPE] || "").toLowerCase() !== TypeConstant.TASK_TYPE.toLowerCase())
227
+ .filter((w) => getChildWorkItemIdsFromRelations(w).length > 0)
228
+ .map((w) => w.id);
229
+ }
230
+
231
+ /**
232
+ * Find a task by its ID across multiple buckets. (e.g., newTasks, existingTasks, deletedTasks)
233
+ *
234
+ * @param did
235
+ * @param buckets
236
+ * @returns The found task or null if not found.
237
+ */
238
+ export function findTaskById(did: string, ...buckets: any[][]) {
239
+ const [, , raw] = did.split(":");
240
+ for (const b of buckets) {
241
+ const t = b.find(x => String(x.id ?? x.tempId) === raw);
242
+ if (t) return t;
243
+ }
244
+ return null;
245
+ };
@@ -0,0 +1,5 @@
1
+ export enum TimeFrame {
2
+ Past = 0,
3
+ Current = 1,
4
+ Future = 2
5
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Node",
8
+ "noImplicitAny": false,
9
+ "baseUrl": ".",
10
+ "paths": {
11
+ "features/*": ["src/features/*"],
12
+ "core/*": ["src/core/*"],
13
+ "app/*": ["src/app/*"],
14
+ "src/*": ["src/*"]
15
+ }
16
+ },
17
+ "include": ["src"]
18
+ }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: [
7
+ {
8
+ find: "azure-devops-extension-api/Work",
9
+ replacement: path.resolve(__dirname, "src/test/mocks/azure-devops-extension-api/Work.ts")
10
+ },
11
+ { find: /^features\/(.*)$/, replacement: path.resolve(__dirname, "src/features/$1") },
12
+ { find: /^core\/(.*)$/, replacement: path.resolve(__dirname, "src/core/$1") },
13
+ { find: /^app\/(.*)$/, replacement: path.resolve(__dirname, "src/app/$1") },
14
+ { find: /^src\/(.*)$/, replacement: path.resolve(__dirname, "src/$1") }
15
+ ]
16
+ },
17
+ test: {
18
+ environment: "node",
19
+ globals: true
20
+ }
21
+ });
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # devops-plugin-kit
2
+
3
+ Monorepo for reusable Azure DevOps plugin packages.
4
+
5
+ ## Packages
6
+
7
+ - `@danieli-automation/devops-plugin-features`: shared feature-level state, hooks, and helpers
8
+ - `@danieli-automation/devops-plugin-core`: core plugin utilities and API wrappers
9
+ - `@danieli-automation/create-devops-plugin`: template/bootstrap utility
10
+
11
+ ## Development
12
+
13
+ ```bash
14
+ npm install
15
+ npm run build
16
+ ```
17
+
18
+ ## Create DevOps Plugin
19
+
20
+ Run from this monorepo workspace:
21
+
22
+ ```bash
23
+ npm run build --workspace @danieli-automation/create-devops-plugin
24
+ npx create-devops-plugin CapacityPlanner --publisher danieli-automation --target-dir ./generated --extension-id capacity-planner --no-install --force
25
+ ```
26
+
27
+ Install and use in another project:
28
+
29
+ ```bash
30
+ npm i -D @danieli-automation/create-devops-plugin
31
+ npx create-devops-plugin MyPlugin --publisher danieli-automation --target-dir ./plugins
32
+ ```
33
+
34
+ `npm create` style (after publish):
35
+
36
+ ```bash
37
+ npm create @danieli-automation/devops-plugin -- MyPlugin --publisher danieli-automation --target-dir ./plugins
38
+ ```
39
+
40
+ Example parameters for `createPluginTemplate(...)`:
41
+
42
+ - `targetDir`: parent folder where the plugin folder will be created
43
+ - `pluginName`: project/plugin folder name and display name
44
+ - `publisher`: Azure DevOps publisher id
45
+ - `extensionId` (optional): manifest id (defaults from `pluginName`)
46
+ - `installDependencies` (optional): runs `npm install` in generated plugin (default `true`)
47
+ - `force` (optional): allows writing into non-empty target folder (default `false`)
48
+
49
+ ## Publish
50
+
51
+ ```bash
52
+ npm publish --workspace @danieli-automation/devops-plugin-features
53
+ npm publish --workspace @danieli-automation/devops-plugin-core
54
+ npm publish --workspace @danieli-automation/create-devops-plugin
55
+ ```
Binary file
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "devops-plugin-kit",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "description": "Shared toolkit for Azure DevOps plugins",
6
+ "author": "Danieli Automation",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "workspaces": [
10
+ "@danieli-automation/*"
11
+ ],
12
+ "scripts": {
13
+ "build": "npm run -ws build",
14
+ "clean": "npm run -ws clean",
15
+ "lint": "npm run -ws lint",
16
+ "test": "npm run -ws test"
17
+ },
18
+ "devDependencies": {
19
+ "@tanstack/react-query": "^4.40.0",
20
+ "@types/node": "^24.3.0",
21
+ "@types/react": "^19.2.14",
22
+ "@types/react-dom": "^19.2.3",
23
+ "azure-devops-extension-api": "^4.251.0",
24
+ "azure-devops-extension-sdk": "^4.0.2",
25
+ "react-query": "^3.39.3",
26
+ "typescript": "^5.9.2",
27
+ "zustand": "4.5.7"
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node"]
14
+ }
15
+ }