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,116 @@
1
+ /**
2
+ * Generates the shared webpack config template entry.
3
+ */
4
+ export function webpackCommonConfigFile(): [string, string] {
5
+ return [
6
+ "config/webpack/common.cjs",
7
+ `const path = require("path");
8
+ const webpack = require("webpack");
9
+ const CopyWebpackPlugin = require("copy-webpack-plugin");
10
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
11
+ const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
12
+ const ReactRefreshTypeScript = require("react-refresh-typescript").default;
13
+ const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
14
+ const dotenv = require("dotenv");
15
+
16
+ dotenv.config({ path: path.resolve(__dirname, "../../.env") });
17
+
18
+ function createCommonConfig({ env, name, entry, outputPath, publicPath, copyPatterns, withDevServer }) {
19
+ const isServe = !!(process.env.WEBPACK_SERVE || process.env.WEBPACK_DEV_SERVER);
20
+ const resolvedEnv = env || process.env.ENV || (isServe ? "dev" : "prod");
21
+ const isDev = resolvedEnv === "dev" && isServe;
22
+ const port = Number(process.env.PORT || 8080);
23
+
24
+ const config = {
25
+ name,
26
+ target: "web",
27
+ entry,
28
+ output: {
29
+ filename: "[name].js",
30
+ path: outputPath,
31
+ publicPath,
32
+ clean: false
33
+ },
34
+ mode: isDev ? "development" : "production",
35
+ devtool: isDev ? "inline-source-map" : "source-map",
36
+ resolve: {
37
+ extensions: [".ts", ".tsx", ".js", ".json"],
38
+ plugins: [
39
+ new TsconfigPathsPlugin({
40
+ configFile: path.resolve(__dirname, "../../tsconfig.json")
41
+ })
42
+ ]
43
+ },
44
+ module: {
45
+ rules: [
46
+ {
47
+ test: /\\.tsx?$/,
48
+ exclude: /node_modules/,
49
+ use: [
50
+ {
51
+ loader: "ts-loader",
52
+ options: {
53
+ transpileOnly: true,
54
+ getCustomTransformers: () => ({
55
+ before: isDev ? [ReactRefreshTypeScript()] : []
56
+ })
57
+ }
58
+ }
59
+ ]
60
+ },
61
+ { test: /\\.css$/, use: ["style-loader", "css-loader"] },
62
+ { test: /\\.scss$/, use: ["style-loader", "css-loader", "sass-loader"], exclude: /node_modules/ },
63
+ { test: /\\.svg$/i, type: "asset/inline" },
64
+ {
65
+ test: /\\.(woff2?|eot|ttf|otf)$/i,
66
+ type: "asset/resource",
67
+ generator: { filename: "static/fonts/[name][ext]" }
68
+ },
69
+ {
70
+ test: /\\.(png|jpe?g|gif|webp)$/i,
71
+ type: "asset/resource",
72
+ generator: { filename: "static/images/[name][ext]" }
73
+ }
74
+ ]
75
+ },
76
+ plugins: [
77
+ new webpack.EnvironmentPlugin({
78
+ ENV: resolvedEnv,
79
+ HMR: isServe ? "true" : "false",
80
+ PORT: String(port),
81
+ SCOPE: process.env.SCOPE || "User"
82
+ }),
83
+ new CopyWebpackPlugin({ patterns: copyPatterns }),
84
+ ...(isDev ? [new ReactRefreshWebpackPlugin({ overlay: false })] : []),
85
+ ...(isDev ? [new ForkTsCheckerWebpackPlugin({ async: true })] : [])
86
+ ]
87
+ };
88
+
89
+ if (withDevServer) {
90
+ config.devServer = {
91
+ server: { type: "https" },
92
+ hot: true,
93
+ liveReload: false,
94
+ static: { directory: path.resolve(__dirname, "../../dist") },
95
+ port,
96
+ open: true,
97
+ historyApiFallback: { index: "/app/App.html" },
98
+ headers: {
99
+ "Access-Control-Allow-Origin": "*",
100
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
101
+ "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
102
+ },
103
+ client: {
104
+ overlay: { errors: true, warnings: false },
105
+ webSocketURL: { protocol: "wss", hostname: "localhost", port }
106
+ }
107
+ };
108
+ }
109
+
110
+ return config;
111
+ }
112
+
113
+ module.exports = { createCommonConfig };
114
+ `
115
+ ];
116
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Generates the webpack root config template entry.
3
+ */
4
+ export function webpackConfigFile(): [string, string] {
5
+ return [
6
+ "webpack.config.cjs",
7
+ `const { createAppConfig } = require("./config/webpack/app.cjs");
8
+
9
+ module.exports = (env = {}) => {
10
+ const configEnv = env.ENV || process.env.ENV;
11
+ return [createAppConfig(configEnv)];
12
+ };
13
+ `
14
+ ];
15
+ }
@@ -0,0 +1 @@
1
+ export { createTemplateFiles } from "./files/index.js";
@@ -0,0 +1,85 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /**
6
+ * Ensures the target output directory exists and respects overwrite rules.
7
+ *
8
+ * @param outputDir - absolute path of the scaffold destination
9
+ * @param force - allows reuse of non-empty target directory when true
10
+ * @throws Error if the target directory is non-empty and force is false
11
+ */
12
+ export async function ensureOutputDir(outputDir: string, force: boolean) {
13
+ try {
14
+ const entries = await fs.readdir(outputDir);
15
+ if (entries.length > 0 && !force) {
16
+ throw new Error(`Target directory is not empty: ${outputDir}`);
17
+ }
18
+ } catch (error: unknown) {
19
+ const code = (error as NodeJS.ErrnoException)?.code;
20
+ if (code !== "ENOENT") {
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ await fs.mkdir(outputDir, { recursive: true });
26
+ }
27
+
28
+ /**
29
+ * Writes all generated template files into the target directory.
30
+ *
31
+ * @param outputDir - absolute project output directory
32
+ * @param files - map of relative file paths and file contents
33
+ * @throws Error if any file or intermediate directory cannot be created
34
+ */
35
+ export async function writeFiles(outputDir: string, files: Map<string, string>) {
36
+ await Promise.all(
37
+ Array.from(files.entries()).map(async ([filePath, content]) => {
38
+ const fullPath = path.join(outputDir, filePath);
39
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
40
+ await fs.writeFile(fullPath, content, "utf8");
41
+ })
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Copies the static folder from the template to the target directory. It checks multiple candidate locations for the source static folder to ensure it can be found regardless of how the package is installed or executed.
47
+ *
48
+ * @param targetDir - target directory where the static folder should be copied
49
+ * @throws Error if the static source folder cannot be found in any of the expected locations
50
+ */
51
+ export async function copyStaticFolder(targetDir: string) {
52
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
53
+ const sourceStaticDir = await resolveStaticSourceDir(moduleDir);
54
+ const destinationStaticDir = path.join(targetDir, "static");
55
+
56
+ await fs.mkdir(destinationStaticDir, { recursive: true });
57
+ await fs.cp(sourceStaticDir, destinationStaticDir, { recursive: true, force: true });
58
+ }
59
+
60
+ /**
61
+ * Resolves the static template source directory from known candidate paths.
62
+ *
63
+ * @param moduleDir - directory of the current module file
64
+ * @throws Error if no candidate static directory exists
65
+ */
66
+ async function resolveStaticSourceDir(moduleDir: string) {
67
+ const candidates = [
68
+ path.resolve(moduleDir, "static"),
69
+ path.resolve(process.cwd(), "@danieli-automation/create-devops-plugin/dist/template/static"),
70
+ path.resolve(process.cwd(), "@danieli-automation/create-devops-plugin/src/template/static"),
71
+ path.resolve(process.cwd(), "dist/template/static"),
72
+ path.resolve(process.cwd(), "src/template/static")
73
+ ];
74
+
75
+ for (const candidate of candidates) {
76
+ try {
77
+ await fs.access(candidate);
78
+ return candidate;
79
+ } catch {
80
+ // try next candidate
81
+ }
82
+ }
83
+
84
+ throw new Error(`Static template folder not found. Checked: ${candidates.join(", ")}`);
85
+ }
@@ -0,0 +1,40 @@
1
+ import type { TemplateContext } from "./types.js";
2
+
3
+ /**
4
+ * Creates production and development Azure DevOps manifest objects.
5
+ *
6
+ * @param context - normalized plugin metadata used for manifest generation
7
+ */
8
+ export function createManifests(context: TemplateContext) {
9
+ const manifest = {
10
+ manifestVersion: 1,
11
+ id: context.extensionId,
12
+ publisher: context.publisher,
13
+ version: "0.0.1",
14
+ name: context.pluginName,
15
+ description: `${context.pluginName} Azure DevOps extension`,
16
+ public: false,
17
+ targets: [{ id: "Microsoft.VisualStudio.Services" }],
18
+ categories: ["Code"],
19
+ files: [{ path: "dist", addressable: true }],
20
+ contributions: [
21
+ {
22
+ id: `${context.extensionId}-hub`,
23
+ type: "ms.vss-web.hub",
24
+ description: `${context.pluginName} hub`,
25
+ targets: ["ms.vss-code-web.code-hub-group"],
26
+ properties: {
27
+ name: context.pluginName,
28
+ uri: "dist/index.html"
29
+ }
30
+ }
31
+ ]
32
+ };
33
+
34
+ const devManifest = {
35
+ ...manifest,
36
+ baseUri: "https://localhost:3000"
37
+ };
38
+
39
+ return { manifest, devManifest };
40
+ }
@@ -0,0 +1,22 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ /**
4
+ * Runs npm install in the generated plugin directory.
5
+ *
6
+ * @param cwd - generated project directory path
7
+ * @throws Error if npm install exits with a non-zero code
8
+ */
9
+ export function runNpmInstall(cwd: string) {
10
+ return new Promise<void>((resolve, reject) => {
11
+ const child = spawn("npm", ["install"], { cwd, stdio: "inherit", shell: true });
12
+ child.on("error", reject);
13
+ child.on("exit", (code) => {
14
+ if (code === 0) {
15
+ resolve();
16
+ return;
17
+ }
18
+
19
+ reject(new Error(`npm install failed with code ${code}`));
20
+ });
21
+ });
22
+ }
@@ -0,0 +1,14 @@
1
+ export interface CreatePluginTemplateInput {
2
+ targetDir: string;
3
+ pluginName: string;
4
+ publisher: string;
5
+ extensionId?: string;
6
+ installDependencies?: boolean;
7
+ force?: boolean;
8
+ }
9
+
10
+ export interface TemplateContext {
11
+ pluginName: string;
12
+ publisher: string;
13
+ extensionId: string;
14
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Normalizes text into a lowercase kebab-case identifier.
3
+ *
4
+ * @param value - raw input text to normalize
5
+ */
6
+ export function normalizeId(value: string) {
7
+ return value.toLowerCase().trim().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-");
8
+ }
9
+
10
+ /**
11
+ * Escapes special characters for safe HTML interpolation.
12
+ *
13
+ * @param value - raw string content
14
+ */
15
+ export function escapeForHtml(value: string) {
16
+ return value
17
+ .replace(/&/g, "&amp;")
18
+ .replace(/</g, "&lt;")
19
+ .replace(/>/g, "&gt;")
20
+ .replace(/\"/g, "&quot;")
21
+ .replace(/'/g, "&#39;");
22
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@danieli-automation/devops-plugin-core",
3
+ "version": "0.1.0",
4
+ "description": "Core utilities and API wrappers for Azure DevOps plugins",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "clean": "Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue",
21
+ "lint": "echo lint not configured",
22
+ "test": "echo test not configured"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @description: Azure DevOps clients for various services.
3
+ * Provides functions to get clients for Core, Work Item Tracking, Work, Git, and Graph services.
4
+ * These clients are used to interact with Azure DevOps REST APIs.
5
+ */
6
+
7
+ import { getClient } from "azure-devops-extension-api";
8
+ import { CoreRestClient } from "azure-devops-extension-api/Core";
9
+ import { GitRestClient } from "azure-devops-extension-api/Git";
10
+ import { GraphRestClient } from "azure-devops-extension-api/Graph";
11
+ import { WorkRestClient } from "azure-devops-extension-api/Work";
12
+ import { WorkItemTrackingRestClient } from "azure-devops-extension-api/WorkItemTracking";
13
+
14
+ export const coreClient = () => getClient(CoreRestClient);
15
+ export const witClient = () => getClient(WorkItemTrackingRestClient);
16
+ export const workClient = () => getClient(WorkRestClient);
17
+ export const gitClient = () => getClient(GitRestClient);
18
+ export const graphClient = () => getClient(GraphRestClient);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @description: Key-Value Store abstraction using Azure DevOps Data Manager.
3
+ * Provides load and save methods for storing and retrieving data with specified scope.
4
+ * Handles non-existent keys gracefully by returning undefined on load.
5
+ * Uses default scope from configuration if none is provided.
6
+ * Logs errors during save operations for debugging purposes.
7
+ */
8
+
9
+ import { IExtensionDataManager, IExtensionDataService } from "azure-devops-extension-api";
10
+ import * as SDK from "azure-devops-extension-sdk";
11
+ import { KVStore } from "core/types/KVStoreType";
12
+ import { ScopeType } from "core/types/ScopeType";
13
+ import { DEFAULT_SCOPE } from "./keys";
14
+
15
+ export function createKVStore<T>(key: string, scopeType: ScopeType = DEFAULT_SCOPE): KVStore<T> {
16
+ return {
17
+ async load() {
18
+ try {
19
+ const manager = await getDataManager();
20
+ return await manager.getValue(key, { scopeType }) as T | undefined;
21
+ } catch (err: any) {
22
+ if (err?.typeKey === "DocumentCollectionDoesNotExistException") {
23
+ return undefined;
24
+ }
25
+ if (String(err).includes("404")) return undefined;
26
+ throw err;
27
+ }
28
+ },
29
+ async save(value: T) {
30
+ try {
31
+ const manager = await getDataManager();
32
+ await manager.setValue(key, value, { scopeType });
33
+ } catch (err) {
34
+ console.log(`CrateteKVStore: Error saving key ${key} with scope ${scopeType}`, err);
35
+ throw err;
36
+ }
37
+ }
38
+ };
39
+ }
40
+
41
+ //#region Private Functions
42
+
43
+ /**
44
+ * @description: Azure DevOps Data Manager accessor.
45
+ * Provides a function to get the extension data manager for storing and retrieving extension data.
46
+ * Caches the manager instance for reuse.
47
+ * Handles SDK readiness and service retrieval.
48
+ * Uses extension context to get the appropriate data manager.
49
+ * @returns Promise resolving to IExtensionDataManager instance.
50
+ */
51
+
52
+ let cachedManager: IExtensionDataManager | null = null;
53
+
54
+ async function getDataManager(): Promise<IExtensionDataManager> {
55
+ if (cachedManager) return cachedManager;
56
+
57
+ try { await SDK.ready(); } catch { }
58
+ const extDataService: IExtensionDataService = await SDK.getService<IExtensionDataService>("ms.vss-features.extension-data-service");
59
+ const accessToken = await SDK.getAccessToken();
60
+ const { id: extensionId } = SDK.getExtensionContext();
61
+
62
+ cachedManager = await extDataService.getExtensionDataManager(extensionId, accessToken);
63
+ return cachedManager;
64
+ }
65
+ //#endregion
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @description: Column Preference storage hooks using React Query.
3
+ * Provides hooks for querying and mutating
4
+ * Utilizes zustand stores for persistent storage and caching.
5
+ * Ensures data consistency by updating the query cache on successful mutations.
6
+ */
7
+
8
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
9
+ import { QUERY_KEYS } from "core/storage/keys";
10
+ import { CreateInstanceInput } from "core/types/instance/CreateInstanceInputType";
11
+ import { CrossSprintInstanceType } from "core/types/instance/CrossSprintInstanceType";
12
+ import { DefaultInstanceType } from "core/types/instance/DefaultInstanceType";
13
+ import { UpdateInstanceInput } from "core/types/instance/UpdateInstanceInputType";
14
+ import { createCrossSprintInstance, deleteCrossTeamInstance, loadAllInstances, updateCrossSprintInstance } from "../repositories/instance";
15
+ import { defaultInstanceStore } from "../stores";
16
+
17
+ /** Load all instances (for some picker UI later)
18
+ *
19
+ * Fetches all saved Cross Sprint instances.
20
+ *
21
+ * @returns React Query result containing the instances list.
22
+ */
23
+ export function useCrossSprintInstancesQuery() {
24
+ return useQuery({
25
+ queryKey: [QUERY_KEYS.crossSprint, QUERY_KEYS.instances],
26
+ queryFn: () => loadAllInstances(),
27
+ staleTime: Infinity,
28
+ cacheTime: 1,
29
+ });
30
+ }
31
+
32
+ /** Create new instance
33
+ *
34
+ * Creates a new Cross Sprint instance and appends it to the cached instances list.
35
+ *
36
+ * @returns React Query mutation for creating an instance.
37
+ */
38
+ export function useCreateCrossSprintInstance() {
39
+ const qc = useQueryClient();
40
+ return useMutation({
41
+ mutationFn: (input: CreateInstanceInput) => createCrossSprintInstance(input),
42
+ onSuccess: (created) => {
43
+ qc.setQueryData<CrossSprintInstanceType[] | undefined>(
44
+ [QUERY_KEYS.crossSprint, QUERY_KEYS.instances],
45
+ (prev) => (prev ? [...prev, created] : [created])
46
+ );
47
+ },
48
+ });
49
+ }
50
+
51
+ /** Update existing instance (e.g. add/remove teams)
52
+ *
53
+ * Updates an existing Cross Sprint instance, replaces it in the cached list,
54
+ * and syncs the zustand `currentInstance` if it matches the updated item.
55
+ *
56
+ * @returns React Query mutation for updating an instance.
57
+ */
58
+ export function useUpdateCrossTeamInstance() {
59
+ const qc = useQueryClient();
60
+
61
+ return useMutation({
62
+ mutationFn: (input: UpdateInstanceInput) => updateCrossSprintInstance(input),
63
+ onSuccess: (updated) => {
64
+ if (!updated) return;
65
+
66
+ qc.setQueryData<CrossSprintInstanceType[] | undefined>(
67
+ [QUERY_KEYS.crossSprint, QUERY_KEYS.instances],
68
+ (prev) => prev?.map(i => i.id === updated.id ? updated : i) ?? [updated]
69
+ );
70
+ },
71
+ });
72
+ }
73
+
74
+ /** Delete an instance
75
+ *
76
+ * Deletes an instance, removes it from the cached instances list,
77
+ * clears related preference queries, and resets zustand `currentInstance`
78
+ * if the deleted instance was selected. Also clears default instance preference
79
+ * if it was pointing to the deleted instance.
80
+ *
81
+ * @returns React Query mutation for deleting an instance by id.
82
+ */
83
+ export function useDeleteCrossTeamInstance() {
84
+ const qc = useQueryClient();
85
+
86
+ return useMutation({
87
+ mutationFn: (instanceId: string) => deleteCrossTeamInstance(instanceId),
88
+ onSuccess: async (_, instanceId) => {
89
+ qc.setQueryData<CrossSprintInstanceType[] | undefined>(
90
+ [QUERY_KEYS.crossSprint, QUERY_KEYS.instances],
91
+ (prev) => prev?.filter(i => i.id !== instanceId) ?? []
92
+ );
93
+
94
+ qc.removeQueries({ queryKey: [QUERY_KEYS.prefs, QUERY_KEYS.backlogOrder, instanceId] });
95
+
96
+ try {
97
+ const pref = await defaultInstanceStore.load();
98
+ if (pref?.id === instanceId) {
99
+ await defaultInstanceStore.save({});
100
+ qc.setQueryData([QUERY_KEYS.crossSprint, QUERY_KEYS.defaultInstancePref], {});
101
+ }
102
+ } catch (e) {
103
+ console.warn("Failed to clear default instance pref after delete:", e);
104
+ }
105
+ },
106
+ });
107
+ }
108
+
109
+ //#region Default Instance Hook
110
+
111
+ /** Load default instance preference
112
+ *
113
+ * Loads the persisted default instance preference from storage.
114
+ *
115
+ * @returns React Query result containing the default instance preference.
116
+ */
117
+ export function useDefaultInstanceQuery() {
118
+ return useQuery({
119
+ queryKey: [QUERY_KEYS.crossSprint, QUERY_KEYS.defaultInstancePref],
120
+ queryFn: () => defaultInstanceStore.load(),
121
+ staleTime: Infinity,
122
+ cacheTime: Infinity,
123
+ });
124
+ }
125
+
126
+ /** Save default instance preference
127
+ *
128
+ * Persists the default instance preference and updates the query cache.
129
+ *
130
+ * @param pref The default instance preference object to save.
131
+ * @returns React Query mutation for saving the default preference.
132
+ */
133
+ export function useSaveDefaultInstance() {
134
+ const qc = useQueryClient();
135
+
136
+ return useMutation({
137
+ // always save an object, never null/undefined
138
+ mutationFn: (pref: DefaultInstanceType) => defaultInstanceStore.save(pref),
139
+ onSuccess: (_, pref) => {
140
+ qc.setQueryData([QUERY_KEYS.crossSprint, QUERY_KEYS.defaultInstancePref], pref);
141
+ },
142
+ });
143
+ }
144
+
145
+ //#endregion