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,132 @@
1
+ import { TimeFrame } from "azure-devops-extension-api/Work";
2
+ import type { IterationInfoType } from "../types/IterationInfoType.js";
3
+
4
+ /**
5
+ * Formats an iteration into a short, human-readable label.
6
+ *
7
+ * Output format:
8
+ * YYYY Mon (DD-DD)
9
+ * Example:
10
+ * 2025 Jan (01-14)
11
+ *
12
+ * Notes:
13
+ * - Dates are interpreted and formatted in UTC to avoid timezone-related shifts.
14
+ * - Returns an empty string if the iteration or its start/end dates are missing
15
+ * or invalid.
16
+ *
17
+ * @param iter The iteration information to format.
18
+ * @returns A formatted iteration label or an empty string if invalid.
19
+ */
20
+ export function formatIteration(iter?: IterationInfoType | null) {
21
+ const start = toDate(iter?.startDate);
22
+ const end = toDate(iter?.finishDate);
23
+ if (!start || !end) return "";
24
+
25
+ const year = start.getUTCFullYear();
26
+ const month = start.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
27
+ const pad = (n: number) => n.toString().padStart(2, "0");
28
+
29
+ return `${year} ${month} (${pad(start.getUTCDate())}-${pad(end.getUTCDate())})`;
30
+ }
31
+
32
+ /**
33
+ * Finds the first iteration path whose root segment matches
34
+ * the root segment of the given area path.
35
+ *
36
+ * Root segment definition:
37
+ * - The first path component before the `\` character.
38
+ *
39
+ * Example:
40
+ * areaPath: "Web\\Frontend"
41
+ * iterationPaths: ["Web\\Sprint 1", "Mobile\\Sprint 1"]
42
+ * result: "Web\\Sprint 1"
43
+ *
44
+ * @param iterationPaths A list of iteration paths to search within.
45
+ * @param areaPath The area path used to determine the root segment.
46
+ * @returns The first matching iteration path, or `undefined` if none match.
47
+ */
48
+ export function findByAreaRoot<T extends string>(iterationPaths?: T[] | null, areaPath?: string | null): T | undefined {
49
+ if (!iterationPaths?.length) return undefined;
50
+ if (!areaPath) return undefined;
51
+
52
+ const areaRoot = areaPath.split("\\")[0]?.trim().toLowerCase();
53
+ if (!areaRoot) return undefined;
54
+
55
+ return iterationPaths.find((p) => {
56
+ const root = p.split("\\")[0]?.trim().toLowerCase();
57
+ return root === areaRoot;
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Resolves a stable string key for an iteration.
63
+ *
64
+ * Priority:
65
+ * 1. Iteration ID (most stable across renames)
66
+ * 2. Iteration path (fallback)
67
+ *
68
+ * This key is typically used for:
69
+ * - React Query cache keys
70
+ * - Memoization
71
+ * - Store indexing
72
+ *
73
+ * @param iteration The iteration to derive a key from.
74
+ * @returns A stable iteration key, or `null` if unavailable.
75
+ */
76
+ export function getIterationKey(iteration?: IterationInfoType): string | null {
77
+ return iteration?.id ?? iteration?.path ?? null;
78
+ }
79
+
80
+ /**
81
+ * Builds a user-facing iteration title that includes:
82
+ * - Formatted date range (via `formatIteration`)
83
+ * - Time frame label (Past/Current/Future/Unknown)
84
+ *
85
+ * Example output:
86
+ * " 2025 Jan (01-14) (Current)"
87
+ *
88
+ * @param iteration The iteration to build a display title for.
89
+ * @returns A formatted iteration title string.
90
+ */
91
+ export function getFormattedIterationTitle(iteration: IterationInfoType) {
92
+ return `${formatIteration(iteration)} (${resolveIterationTimeFrame(iteration)})`;
93
+ }
94
+
95
+ //#region Private Functions
96
+ /**
97
+ * Safely converts a date string into a `Date` object.
98
+ *
99
+ * @param v A date string value.
100
+ * @returns A valid `Date` object, or `undefined` if the value is null, undefined,
101
+ * or cannot be parsed into a valid date.
102
+ */
103
+ function toDate(v?: string | null): Date | undefined {
104
+ if (v == null) return undefined;
105
+ const date = new Date(v);
106
+ return isNaN(date.getTime()) ? undefined : date;
107
+ }
108
+
109
+ /**
110
+ * Converts an iteration's `TimeFrame` value into a user-friendly label.
111
+ *
112
+ * Notes:
113
+ * - Treats `undefined` the same as `Past` (common when the API omits the field).
114
+ * - Returns "Unknown" for unexpected enum values.
115
+ *
116
+ * @param iteration The iteration containing the `timeFrame` value.
117
+ * @returns The normalized time frame label ("Past", "Current", "Future", or "Unknown").
118
+ */
119
+ function resolveIterationTimeFrame(iteration: IterationInfoType): string {
120
+ switch (iteration.timeFrame) {
121
+ case TimeFrame.Past:
122
+ case undefined:
123
+ return "Past";
124
+ case TimeFrame.Current:
125
+ return "Current";
126
+ case TimeFrame.Future:
127
+ return "Future";
128
+ default:
129
+ return "Unknown";
130
+ }
131
+ }
132
+ //#endregion
@@ -0,0 +1,79 @@
1
+ import { TeamProjectReference, WebApiTeam } from "azure-devops-extension-api/Core";
2
+ import { coreClient } from "core/azureClients";
3
+ import { SelectedProjectType } from "features/teams/types/SelectedProjectType";
4
+
5
+ type ListBoxItem = {
6
+ id?: string;
7
+ text?: string;
8
+ [key: string]: unknown;
9
+ };
10
+
11
+ /**
12
+ * Fetches all projects and maps them to IListBoxItem objects.
13
+ *
14
+ * @description This function retrieves all projects using the Core REST client
15
+ * and maps them to an array of IListBoxItem for UI representation.
16
+ *
17
+ * @returns An object containing an array of IListBoxItem and an array of TeamProjectReference
18
+ */
19
+ export async function fetchProjects() {
20
+ const core = coreClient();
21
+ const projects: TeamProjectReference[] = await core.getProjects();
22
+ const projectItems = projects.map((project) => ({ text: project.name, id: project.id, })) as ListBoxItem[];
23
+ return { projectItems, projects };
24
+ }
25
+
26
+ /**
27
+ * Fetches a single project by its ID.
28
+ * @description This function retrieves a specific project using the Core REST client
29
+ * and returns its TeamProjectReference.
30
+ *
31
+ * @param projectId - The unique identifier of the project to fetch
32
+ * @returns A promise resolving to the TeamProjectReference of the project, or null if not found
33
+ */
34
+ export async function fetchProjectById(projectId: string): Promise<any | null> {
35
+ const core = coreClient();
36
+ const project = await core.getProject(projectId);
37
+ return project;
38
+ }
39
+
40
+ /**
41
+ * Fetches project and team IDs based on team project name, selected projects, and area path.
42
+ *
43
+ * @param teamProjectName The name of the team project
44
+ * @param selectedProjects An array of selected projects
45
+ * @param areaPath The area path within the project
46
+ * @returns Promise resolving to an object containing projectId and teamId
47
+ *
48
+ */
49
+ export async function getProjectAndTeamIds(teamProjectName: string, selectedProjects: SelectedProjectType[], areaPath?: string) {
50
+ const core = coreClient();
51
+
52
+ const project = await core.getProject(teamProjectName);
53
+ const projectId = project.id;
54
+
55
+ const parts = (areaPath ?? "").split("\\");
56
+ const candidateTeamName = parts.length > 1 ? parts[1] : undefined;
57
+ const teams: WebApiTeam[] = await core.getTeams(projectId, undefined, 200, 0);
58
+
59
+ teams.filter(t => {
60
+ return selectedProjects.some(p => {
61
+ return p.teamId === t.id;
62
+ });
63
+ });
64
+
65
+ let team = (candidateTeamName && teams.find(t => t.name?.toLowerCase() === candidateTeamName.toLowerCase())) ||
66
+ (teams as any[]).find(t => t.isDefault) ||
67
+ teams[0];
68
+
69
+ if (!team?.id) {
70
+ throw new Error(`No team found in project ${project.name}`);
71
+ }
72
+
73
+ return {
74
+ projectId,
75
+ teamId: team.id,
76
+ projectName: project.name,
77
+ teamName: team.name,
78
+ };
79
+ }
@@ -0,0 +1,29 @@
1
+ import { coreClient } from "core/azureClients";
2
+
3
+ /**
4
+ * Fetches teams for a given project.
5
+ * @description This function retrieves all teams within the specified project
6
+ * using the Core REST client and maps them to IListBoxItem format.
7
+ *
8
+ * @param projectId - The ID of the project to fetch teams for
9
+ * @returns - A promise resolving to an array of teams in IListBoxItem format
10
+ */
11
+ export async function fetchTeams(projectId: string): Promise<any[]> {
12
+ const core = coreClient();
13
+ const teams = await core.getTeams(projectId);
14
+ return teams.map((team) => ({ text: team.name, id: team.id }));
15
+ }
16
+
17
+ /**
18
+ * Fetches a team by its ID within a project.
19
+ * @description This function retrieves a specific team using the Core REST client
20
+ * and returns its details.
21
+ *
22
+ * @param projectId - Project ID
23
+ * @param teamId - Team ID
24
+ * @returns - A promise resolving to the team details
25
+ */
26
+ export function fetchTeamById(projectId: string, teamId: string): Promise<any> {
27
+ const core = coreClient();
28
+ return core.getTeam(projectId, teamId);
29
+ }
@@ -0,0 +1,124 @@
1
+ import { GraphMember } from "azure-devops-extension-api/Graph";
2
+ import { AvatarSize } from "azure-devops-extension-api/Profile/Profile";
3
+ import { coreClient, graphClient } from "core/azureClients";
4
+ import { SelectedProjectType } from "features/teams/types/SelectedProjectType";
5
+ import { UserSearchResultType } from "features/teams/types/UserSearchResultType";
6
+ import { getProjectAndTeamIds } from "./projects.js";
7
+
8
+ /**
9
+ * Fetches current team members for a given team project and selected projects.
10
+ *
11
+ * @param teamProjectName
12
+ * @param selectedProjects
13
+ * @param areaPath
14
+ * @returns Promise resolving to an array of team member information
15
+ *
16
+ */
17
+ export async function getCurrentTeamMembers(teamProjectName: string, selectedProjects: SelectedProjectType[], areaPath?: string) {
18
+ const core = coreClient();
19
+ const ids = await getProjectAndTeamIds(teamProjectName, selectedProjects, areaPath);
20
+ let members = await core.getTeamMembersWithExtendedProperties(ids.projectId, ids.teamId);
21
+
22
+ /*if (!isAdminIncluded) {
23
+ members = members.filter(m => !m.isTeamAdmin);
24
+ }*/
25
+
26
+ return members.map(m => ({
27
+ descriptor: m.identity?.descriptor,
28
+ displayName: m.identity?.displayName ?? "",
29
+ uniqueName: m.identity?.uniqueName,
30
+ imageUrl: m.identity?.imageUrl,
31
+ }));
32
+ }
33
+
34
+ /**
35
+ * Fetches the avatar image for a given Azure DevOps user.
36
+ *
37
+ * Retrieves the user's avatar using their graph descriptor and
38
+ * allows specifying the desired avatar size.
39
+ *
40
+ * @param descriptor Azure DevOps graph descriptor of the user.
41
+ * @param size Optional avatar size (defaults to small).
42
+ * @returns Promise resolving to the user's avatar image.
43
+ */
44
+ export async function getDefaultUserAvatar(descriptor?: string, size = AvatarSize.Small as AvatarSize) {
45
+ if (!descriptor) return;
46
+ return await graphClient().getAvatar(descriptor, size);
47
+ }
48
+
49
+ /**
50
+ * Searches users across the whole organization for typeahead scenarios.
51
+ *
52
+ * Uses Graph client `querySubjects` with `subjectKind: ["User"]`.
53
+ * Azure DevOps Graph currently returns results in batches (typically up to 100).
54
+ *
55
+ * @param query searched value
56
+ * @param maxResults desired max result constant
57
+ * @returns Promise resolving to an object containing user fields
58
+ */
59
+ export async function searchUsersInOrganization(query: string, maxResults = 100): Promise<UserSearchResultType[]> {
60
+ const term = query.trim();
61
+ if (!term) return [];
62
+
63
+ const subjects = await graphClient().querySubjects({
64
+ query: term,
65
+ subjectKind: ["User"],
66
+ scopeDescriptor: undefined as unknown as string,
67
+ });
68
+
69
+ return (subjects as GraphMember[])
70
+ .filter(subject => (subject.subjectKind || "").toLowerCase() === "user")
71
+ .slice(0, maxResults)
72
+ .map(subject => ({
73
+ id: subject.descriptor,
74
+ descriptor: subject.descriptor,
75
+ displayName: subject.displayName || "",
76
+ uniqueName: subject.principalName || undefined,
77
+ mailAddress: subject.mailAddress || undefined,
78
+ origin: subject.origin || undefined,
79
+ originId: subject.originId || undefined,
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * Resolves user ids/descriptors to user profiles for display purposes.
85
+ *
86
+ * This is used when persisted owner ids are already known, but we need
87
+ * display names for UI rendering.
88
+ *
89
+ * @params userIds list of user ids to get details
90
+ * @returns Promise resolving to an object containing user details
91
+ */
92
+ export async function getUsersByIds(userIds: string[]): Promise<UserSearchResultType[]> {
93
+ const ids = Array.from(new Set((userIds ?? []).map(id => (id || "").trim()).filter(Boolean)));
94
+ if (ids.length === 0) return [];
95
+
96
+ const graph = graphClient();
97
+
98
+ const resolved = await Promise.all(ids.map(async (userId) => {
99
+ let subject: GraphMember | undefined;
100
+
101
+ // Case 1: id is already a graph descriptor.
102
+ try {
103
+ const byDescriptor = await graph.getUser(userId) as GraphMember;
104
+ if ((byDescriptor?.subjectKind || "").toLowerCase() === "user") {
105
+ subject = byDescriptor;
106
+ }
107
+ } catch {
108
+ // noop
109
+ }
110
+
111
+ if (!subject) return undefined;
112
+
113
+ return {
114
+ id: subject.descriptor,
115
+ descriptor: subject.descriptor,
116
+ displayName: subject.displayName || userId,
117
+ uniqueName: subject.principalName || undefined,
118
+ mailAddress: subject.mailAddress || undefined,
119
+ origin: subject.origin || undefined,
120
+ } as UserSearchResultType;
121
+ }));
122
+
123
+ return resolved.filter(Boolean) as UserSearchResultType[];
124
+ }
@@ -0,0 +1,80 @@
1
+ import { workClient } from "core/azureClients";
2
+ import { fetchProjectById } from "features/teams/api/projects";
3
+ import { SelectedProjectType } from "features/teams/types/SelectedProjectType";
4
+ import { fetchTeamById } from "../api/teams.js";
5
+
6
+ export type AreaGroup = {
7
+ projectId: string;
8
+ projectName: string;
9
+ teamId: string;
10
+ teamName?: string;
11
+ areaPaths: string[];
12
+ };
13
+
14
+ const areaPathCache = new Map<string, AreaGroup>();
15
+
16
+ export const ProjectService = {
17
+ getAreaPaths: async (projectTeamPairs?: SelectedProjectType[]): Promise<AreaGroup[]> => {
18
+ if (!projectTeamPairs || projectTeamPairs.length === 0) return [];
19
+
20
+ const results = await Promise.all(
21
+ projectTeamPairs.map(async (pair) => {
22
+
23
+ const { projectId, teamId } = pair;
24
+
25
+ const cacheKey = `${projectId}:${teamId}`;
26
+
27
+ const cached = areaPathCache.get(cacheKey);
28
+
29
+ if (cached) return cached;
30
+
31
+ try {
32
+ const [project, team] = await Promise.all([fetchProjectById(projectId), fetchTeamById(projectId, teamId)]);
33
+ const teamName = team?.name ?? "";
34
+
35
+ const teamContext = {
36
+ project: project?.name ?? "",
37
+ projectId,
38
+ team: teamName,
39
+ teamId,
40
+ };
41
+
42
+ const teamFieldValues = await workClient().getTeamFieldValues(teamContext);
43
+
44
+ const collected: string[] = [];
45
+
46
+ if (Array.isArray(teamFieldValues?.values)) {
47
+ for (const v of teamFieldValues.values) {
48
+ if (!v?.value) continue;
49
+ collected.push(v.value);
50
+ }
51
+ }
52
+
53
+ if (collected.length === 0) {
54
+ console.warn("[getAreaPaths] Team has no area paths configured");
55
+ }
56
+
57
+ const projectName = project?.name ?? "";
58
+
59
+ const clean = Array.from(new Set(collected.filter(p => typeof p === "string" && p.length > 0)
60
+ .map(p => p.replace(/[\u0000-\u001F\u007F-\u009F]/g, "").trim()))
61
+ );
62
+
63
+ const specificAreaPaths = clean.filter(p => p === projectName || p.startsWith(projectName + "\\"));
64
+
65
+ const result = { projectId, projectName, teamId, teamName, areaPaths: specificAreaPaths } as AreaGroup;
66
+ areaPathCache.set(cacheKey, result);
67
+ return result;
68
+
69
+ } catch (err) {
70
+ //console.error("[getAreaPaths] Failed to fetch area paths");
71
+
72
+ const result = { projectId, projectName: "", teamId, teamName: undefined, areaPaths: [] } as AreaGroup;
73
+ areaPathCache.set(cacheKey, result);
74
+ return result;
75
+ }
76
+ })
77
+ );
78
+ return results;
79
+ },
80
+ };
@@ -0,0 +1,8 @@
1
+ export type SelectedProjectType = {
2
+ projectId: string;
3
+ projectName?: string;
4
+ teamName?: string;
5
+ teamId: string;
6
+ teams?: any[],
7
+ selectedTeamKey?: string,
8
+ };
@@ -0,0 +1,7 @@
1
+ export type TeamMember = {
2
+ id: string;
3
+ descriptor?: string;
4
+ displayName: string;
5
+ uniqueName: string;
6
+ imageUrl?: string;
7
+ };
@@ -0,0 +1,6 @@
1
+ export type TeamRowSeedType = {
2
+ projectId?: string;
3
+ projectName?: string;
4
+ teamId?: string;
5
+ teamName?: string;
6
+ };
@@ -0,0 +1,8 @@
1
+ export type UserSearchResultType = {
2
+ id: string;
3
+ descriptor: string;
4
+ displayName: string;
5
+ uniqueName?: string;
6
+ mailAddress?: string;
7
+ origin?: string;
8
+ }
@@ -0,0 +1,24 @@
1
+ import { getClient } from "azure-devops-extension-api";
2
+ import { WorkItemTrackingRestClient } from "azure-devops-extension-api/WorkItemTracking";
3
+
4
+ /**
5
+ * Fetches the work item states for a given work item type within a project.
6
+ * @description This function uses the Work Item Tracking REST client to retrieve
7
+ * the states associated with the specified work item type in the given project.
8
+ *
9
+ * @param projectName - The name of the project
10
+ * @param workItemType - The work item type (e.g., 'Task', 'Bug')
11
+ * @returns - A promise resolving to an array of work item states for the specified type
12
+ */
13
+ export async function getWorkItemStates(projectName: string, workItemType: string) {
14
+ const witClient = getClient(WorkItemTrackingRestClient);
15
+ const typeInfo = await witClient.getWorkItemType(projectName, workItemType);
16
+
17
+ // Map to a clean array for dropdowns
18
+ return typeInfo.states.map((s) => ({
19
+ id: s.name,
20
+ text: s.name,
21
+ color: s.color,
22
+ category: s.category
23
+ }));
24
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Fetches child work items for a given parent work item within a project.
3
+ *
4
+ * @param pbiIds - Array of parent work item IDs (PBIs or Bugs)
5
+ * @returns WIQL string to fetch child work items
6
+ *
7
+ */
8
+ export function wiqlForParentWorkItems(pbiIds: number[]) {
9
+ if (pbiIds.length === 0) return null;
10
+
11
+ const idsClause = pbiIds.length === 1 ? `[Target].[System.Id] = ${pbiIds[0]}` : `[Target].[System.Id] IN (${pbiIds.join(',')})`;
12
+
13
+ const wiql = `
14
+ SELECT [Source].[System.Id]
15
+ FROM WorkItemLinks
16
+ WHERE
17
+ (
18
+ [Source].[System.WorkItemType] IN ('Epic','Feature')
19
+ AND [Source].[System.State] <> 'Removed'
20
+ )
21
+ AND ([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward')
22
+ AND (
23
+ [Target].[System.WorkItemType] IN ('Product Backlog Item','Bug')
24
+ AND [Target].[System.State] <> 'Removed'
25
+ AND ${idsClause}
26
+ )
27
+ MODE (Recursive) `;
28
+
29
+ return wiql;
30
+ }
31
+
32
+ /**
33
+ * @description: Generates WIQL queries to fetch PBIs/Bugs and Tasks that belong to a single iteration,
34
+ * and optionally restricted to one or more area path roots (UNDER clauses).
35
+ * @param iterationPath The selected iteration path (e.g. "Project\\2026\\Sprint 02").
36
+ * @param areaPaths Optional list of area paths to scope results (uses UNDER).
37
+ * @returns An object containing WIQL strings for parent items and tasks.
38
+ */
39
+ export function wiqlForSingleIteration(iterationPath: string, areaPaths?: (string | null)[]) {
40
+ const safeAreaPaths = (areaPaths ?? []).filter((p): p is string => typeof p === "string" && p.length > 0);
41
+
42
+ const areaClause = safeAreaPaths.length
43
+ ? `AND (${safeAreaPaths.map(p => `[System.AreaPath] UNDER '${escapeWiqlString(p)}'`).join(" OR ")})`
44
+ : "";
45
+
46
+ const iterClause = `[System.IterationPath] = '${escapeWiqlString(sanitizeForWiql(iterationPath))}'`;
47
+
48
+ const parentsQuery = `
49
+ SELECT [System.Id]
50
+ FROM WorkItems
51
+ WHERE
52
+ [System.WorkItemType] IN ('Product Backlog Item','Bug')
53
+ AND [System.State] <> 'Removed'
54
+ AND ${iterClause}
55
+ ${areaClause}
56
+ `;
57
+
58
+ const tasksQuery = `
59
+ SELECT [System.Id]
60
+ FROM WorkItems
61
+ WHERE
62
+ [System.WorkItemType] = 'Task'
63
+ AND [System.State] <> 'Removed'
64
+ AND ${iterClause}
65
+ ${areaClause}
66
+ `;
67
+
68
+ return { parentsQuery, tasksQuery };
69
+ }
70
+
71
+ /**
72
+ * @description: Generates WIQL queries to fetch PBIs/Bugs and Tasks that belong to multiple iterations,
73
+ * optionally restricted to one or more area path roots (UNDER clauses).
74
+ * @param iterationPaths The selected iteration paths.
75
+ * @param areaPaths Optional list of area paths to scope results (uses UNDER).
76
+ * @returns An object containing WIQL strings for parent items and tasks.
77
+ */
78
+ export function wiqlForMultipleIterations(iterationPaths: string[], areaPaths?: (string | null)[]) {
79
+ const safeAreaPaths = (areaPaths ?? []).filter((p): p is string => typeof p === "string" && p.length > 0);
80
+
81
+ const areaClause = safeAreaPaths.length
82
+ ? `AND (${safeAreaPaths.map(p => `[System.AreaPath] UNDER '${escapeWiqlString(p)}'`).join(" OR ")})`
83
+ : "";
84
+
85
+ const iterClause = iterationPaths && iterationPaths.length
86
+ ? buildUnderOrClause('[System.IterationPath]', iterationPaths)
87
+ : "";
88
+
89
+ const parentsQuery = `
90
+ SELECT [System.Id]
91
+ FROM WorkItems
92
+ WHERE
93
+ [System.WorkItemType] IN ('Product Backlog Item','Bug')
94
+ AND [System.State] <> 'Removed'
95
+ ${iterClause ? 'AND ' + iterClause : ''}
96
+ ${areaClause}
97
+ `;
98
+
99
+ const tasksQuery = `
100
+ SELECT [System.Id]
101
+ FROM WorkItems
102
+ WHERE
103
+ [System.WorkItemType] = 'Task'
104
+ AND [System.State] <> 'Removed'
105
+ ${iterClause ? 'AND ' + iterClause : ''}
106
+ ${areaClause}
107
+ `;
108
+
109
+ return { parentsQuery, tasksQuery };
110
+ }
111
+
112
+ //#region Private Functions
113
+ /**
114
+ * Builds a WIQL clause joining multiple `UNDER` path checks with `OR`.
115
+ *
116
+ * @param field WIQL field name (e.g. `[System.IterationPath]`).
117
+ * @param paths Path list to include.
118
+ * @returns A WIQL condition string, or empty string when no paths are provided.
119
+ */
120
+ function buildUnderOrClause(field: string, paths?: string[]) {
121
+ if (!paths || paths.length === 0) return '';
122
+ const cleanPaths = paths.map(p => escapeWiqlString(sanitizeForWiql(p)));
123
+ if (cleanPaths.length === 1) return `${field} UNDER '${cleanPaths[0]}'`;
124
+ return '(' + cleanPaths.map(p => `${field} UNDER '${p}'`).join(' OR ') + ')';
125
+ }
126
+
127
+ /**
128
+ * Escapes string values for safe embedding in WIQL string literals.
129
+ *
130
+ * @param str Raw string value.
131
+ * @returns Escaped WIQL-safe string.
132
+ */
133
+ function escapeWiqlString(str: string) {
134
+ return str.replace(/'/g, "''").replace(/\\/g, '\\\\');
135
+ }
136
+
137
+ /**
138
+ * Removes control characters and trims whitespace before WIQL interpolation.
139
+ *
140
+ * @param s Raw input string.
141
+ * @returns Sanitized string suitable for WIQL.
142
+ */
143
+ function sanitizeForWiql(s: string) {
144
+ return s ? s.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').trim() : s;
145
+ }
146
+ //#endregion