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.
- package/@danieli-automation/create-devops-plugin/package.json +30 -0
- package/@danieli-automation/create-devops-plugin/src/cli.ts +136 -0
- package/@danieli-automation/create-devops-plugin/src/index.ts +37 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/apiIndexMock.ts +10 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/appHtml.ts +25 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/appStyles.ts +22 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/appTsx.ts +41 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/envExample.ts +11 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/gitignore.ts +13 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/index.ts +51 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/packageJson.ts +60 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/readme.ts +30 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/sdkMock.ts +11 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/testSetup.ts +16 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/tsconfigJson.ts +30 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/vitestConfig.ts +57 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/webpackAppConfig.ts +31 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/webpackCommonConfig.ts +116 -0
- package/@danieli-automation/create-devops-plugin/src/template/files/webpackConfig.ts +15 -0
- package/@danieli-automation/create-devops-plugin/src/template/files.ts +1 -0
- package/@danieli-automation/create-devops-plugin/src/template/fs.ts +85 -0
- package/@danieli-automation/create-devops-plugin/src/template/manifest.ts +40 -0
- package/@danieli-automation/create-devops-plugin/src/template/npm.ts +22 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/fonts/AzDevMDL2.woff +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/fonts/bowtie.woff2 +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/fonts/fabric-icons.woff +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/fonts/fluent-filled-v1.1.293.woff2 +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/fonts/fluent-regular-v1.1.293.woff2 +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/images/DigiMetLogo.jpeg +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/images/danieliAutomation.png +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/images/danieliAutomationBlack.jpg +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/images/danieli_digi_met_logo.jpeg +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/static/images/logoSmallpng.png +0 -0
- package/@danieli-automation/create-devops-plugin/src/template/types.ts +14 -0
- package/@danieli-automation/create-devops-plugin/src/template/utils.ts +22 -0
- package/@danieli-automation/create-devops-plugin/tsconfig.json +8 -0
- package/@danieli-automation/devops-plugin-core/package.json +27 -0
- package/@danieli-automation/devops-plugin-core/src/core/azureClients.ts +18 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/createStore.ts +65 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/hooks/useCrossTeamSprintInstance.ts +145 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/hooks/useTaskOrder.ts +125 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/hooks/useWorkItemOrder.ts +86 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/index.ts +13 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/keys.ts +31 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/repositories/instance.ts +184 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/repositories/taskOrder.ts +59 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/repositories/workItemOrder.ts +60 -0
- package/@danieli-automation/devops-plugin-core/src/core/storage/stores.ts +18 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/AdoWorkItemType.ts +18 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/KVStoreType.ts +1 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/ScopeType.ts +1 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/SelectedProjectType.ts +8 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/SortConfigType.ts +2 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/instance/CreateInstanceInputType.ts +10 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/instance/CrossSprintInstanceType.ts +20 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/instance/DefaultInstanceType.ts +12 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/instance/InstanceRowType.ts +18 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/instance/UpdateInstanceInputType.ts +10 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/taskOrder/TaskOrderMapType.ts +3 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/taskOrder/TaskOrderType.ts +1 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/workItemOrder/WorkItemOrderMapType.ts +3 -0
- package/@danieli-automation/devops-plugin-core/src/core/types/workItemOrder/WorkItemOrderType.ts +1 -0
- package/@danieli-automation/devops-plugin-core/src/index.ts +1 -0
- package/@danieli-automation/devops-plugin-core/src/pluginCore.ts +12 -0
- package/@danieli-automation/devops-plugin-core/tsconfig.json +16 -0
- package/@danieli-automation/devops-plugin-features/package.json +31 -0
- package/@danieli-automation/devops-plugin-features/src/app/stores/useUIStore.ts +12 -0
- package/@danieli-automation/devops-plugin-features/src/app/utils/date.ts +9 -0
- package/@danieli-automation/devops-plugin-features/src/app/utils/global.ts +9 -0
- package/@danieli-automation/devops-plugin-features/src/core/azureClients.ts +12 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/constants/InstanceConstant.ts +5 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/hooks/useInstancePermission.ts +127 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/stores/__tests__/useInstanceStore.test.ts +25 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/stores/types/InstanceStoreType.ts +7 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/stores/useInstanceStore.ts +9 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/types/CrossSprintInstanceType.ts +20 -0
- package/@danieli-automation/devops-plugin-features/src/features/instances/utils/instance.ts +55 -0
- package/@danieli-automation/devops-plugin-features/src/features/iterations/api/iterations.ts +298 -0
- package/@danieli-automation/devops-plugin-features/src/features/iterations/services/IterationService.ts +215 -0
- package/@danieli-automation/devops-plugin-features/src/features/iterations/types/IterationInfoType.ts +15 -0
- package/@danieli-automation/devops-plugin-features/src/features/iterations/utils/__tests__/iteration.test.ts +39 -0
- package/@danieli-automation/devops-plugin-features/src/features/iterations/utils/iteration.ts +132 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/api/projects.ts +79 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/api/teams.ts +29 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/api/users.ts +124 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/services/ProjectService.ts +80 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/types/SelectedProjectType.ts +8 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/types/TeamMemberType.ts +7 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/types/TeamRowSeedType.ts +6 -0
- package/@danieli-automation/devops-plugin-features/src/features/teams/types/UserSearchResultType.ts +8 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/api/states.ts +24 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/api/wiql.ts +146 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/api/workItems.ts +193 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/DefaultConsant.ts +43 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/FieldConstant.ts +27 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/StateConstant.ts +16 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/constants/TypeConstant.ts +8 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/services/WIQLService.ts +101 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/services/WorkItemService.ts +54 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/types/AdoWorkItemType.ts +18 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/__tests__/filter.test.ts +87 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/__tests__/workItem.test.ts +116 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/filter.ts +189 -0
- package/@danieli-automation/devops-plugin-features/src/features/work-items/utils/workItem.ts +245 -0
- package/@danieli-automation/devops-plugin-features/src/index.ts +0 -0
- package/@danieli-automation/devops-plugin-features/src/test/mocks/azure-devops-extension-api/Work.ts +5 -0
- package/@danieli-automation/devops-plugin-features/tsconfig.json +18 -0
- package/@danieli-automation/devops-plugin-features/vitest.config.ts +21 -0
- package/README.md +55 -0
- package/devops-plugin-kit-0.1.0.tgz +0 -0
- package/package.json +29 -0
- 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
|
+
}
|
package/@danieli-automation/devops-plugin-features/src/features/teams/services/ProjectService.ts
ADDED
|
@@ -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,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
|