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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description: Task order storage hooks using React Query.
|
|
3
|
+
* Provides hooks for querying and mutating task order preferences.
|
|
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 { TaskOrderType } from "core/types/taskOrder/TaskOrderType";
|
|
10
|
+
import { AdoWorkItemType } from "core/types/AdoWorkItemType";
|
|
11
|
+
import { QUERY_KEYS, STORAGE_KEYS } from "../keys";
|
|
12
|
+
import { loadTaskOrder, saveTaskOrder } from "../repositories/taskOrder";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load task ordering preference for a given instance.
|
|
16
|
+
*
|
|
17
|
+
* @param instanceId The instance id to scope the task order to.
|
|
18
|
+
* @returns React Query result containing the task order.
|
|
19
|
+
*/
|
|
20
|
+
export function useTaskOrderQuery(instanceId: string | null | undefined) {
|
|
21
|
+
const isSharedInstance = !!instanceId;
|
|
22
|
+
|
|
23
|
+
return useQuery({
|
|
24
|
+
queryKey: [QUERY_KEYS.prefs, QUERY_KEYS.taskOrder, instanceId],
|
|
25
|
+
enabled: isSharedInstance,
|
|
26
|
+
queryFn: () => loadTaskOrder(instanceId),
|
|
27
|
+
staleTime: Infinity,
|
|
28
|
+
cacheTime: 1,
|
|
29
|
+
keepPreviousData: false,
|
|
30
|
+
// Keep shared-instance task order in sync across owners/viewers.
|
|
31
|
+
refetchInterval: isSharedInstance ? 5000 : false,
|
|
32
|
+
refetchOnWindowFocus: isSharedInstance,
|
|
33
|
+
refetchOnReconnect: isSharedInstance,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Save task ordering preference
|
|
39
|
+
*
|
|
40
|
+
* Persists task order rows for the given instance (or personal scope),
|
|
41
|
+
* and updates the query cache on success.
|
|
42
|
+
*
|
|
43
|
+
* @param instanceId The instance id or null for personal order.
|
|
44
|
+
* @returns React Query mutation for saving task order.
|
|
45
|
+
*/
|
|
46
|
+
export function useSaveTaskOrder(instanceId: string | null | undefined) {
|
|
47
|
+
const qc = useQueryClient();
|
|
48
|
+
const effectiveId = instanceId ?? STORAGE_KEYS.personalTaskOrder;
|
|
49
|
+
return useMutation({
|
|
50
|
+
mutationFn: (rows: TaskOrderType[]) => saveTaskOrder(effectiveId, rows),
|
|
51
|
+
onSuccess: (_, rows) => {
|
|
52
|
+
qc.setQueryData([QUERY_KEYS.prefs, QUERY_KEYS.taskOrder, effectiveId], rows);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildNormalizedTaskOrder(tasksInCurrentOrder: AdoWorkItemType[], stored: TaskOrderType[]): TaskOrderType[] {
|
|
58
|
+
if (!tasksInCurrentOrder.length) return [];
|
|
59
|
+
|
|
60
|
+
const byParent = new Map<number, AdoWorkItemType[]>();
|
|
61
|
+
for (const t of tasksInCurrentOrder) {
|
|
62
|
+
if (typeof t.id !== "number") continue;
|
|
63
|
+
const pid = getParentIdFromTask(t);
|
|
64
|
+
if (pid == null || Number.isNaN(pid)) continue;
|
|
65
|
+
const arr = byParent.get(pid) ?? [];
|
|
66
|
+
arr.push(t);
|
|
67
|
+
byParent.set(pid, arr);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const storedByParent = new Map<number, TaskOrderType[]>();
|
|
71
|
+
for (const r of stored ?? []) {
|
|
72
|
+
const arr = storedByParent.get(r.parentId) ?? [];
|
|
73
|
+
arr.push(r);
|
|
74
|
+
storedByParent.set(r.parentId, arr);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalized: TaskOrderType[] = [];
|
|
78
|
+
|
|
79
|
+
Array.from(byParent.entries()).forEach(([parentId, tasks]) => {
|
|
80
|
+
const storedRows = storedByParent.get(parentId) ?? [];
|
|
81
|
+
const byId = new Map(storedRows.map(r => [r.id, r.order]));
|
|
82
|
+
|
|
83
|
+
const looksIdBasedOrMissing =
|
|
84
|
+
storedRows.length > 0 &&
|
|
85
|
+
storedRows.every(r => !r.order || r.order === r.id);
|
|
86
|
+
|
|
87
|
+
let ordered = tasks.map((t) => ({
|
|
88
|
+
id: t.id as number,
|
|
89
|
+
order: byId.get(t.id as number) ?? Number.MAX_SAFE_INTEGER,
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
if (looksIdBasedOrMissing || storedRows.length === 0) {
|
|
93
|
+
ordered = tasks.map((t, idx) => ({ id: t.id as number, order: idx + 1 }));
|
|
94
|
+
} else {
|
|
95
|
+
ordered.sort((a, b) => a.order - b.order || a.id - b.id);
|
|
96
|
+
ordered = ordered.map((r, idx) => ({ id: r.id, order: idx + 1 }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ordered.forEach((r) => {
|
|
100
|
+
normalized.push({
|
|
101
|
+
id: r.id,
|
|
102
|
+
parentId,
|
|
103
|
+
order: r.order,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getParentIdFromTask(task: AdoWorkItemType): number | null {
|
|
112
|
+
if (typeof task.parentWorkItem?.id === "number") {
|
|
113
|
+
return task.parentWorkItem.id;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parentRel = task.relations?.find((rel) => rel?.rel === "System.LinkTypes.Hierarchy-Reverse");
|
|
117
|
+
if (!parentRel?.url) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const segments = parentRel.url.split("/");
|
|
122
|
+
const idRaw = segments[segments.length - 1];
|
|
123
|
+
const id = Number(idRaw);
|
|
124
|
+
return Number.isFinite(id) ? id : null;
|
|
125
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { WorkItemOrderType } from "core/types/workItemOrder/WorkItemOrderType";
|
|
10
|
+
import { QUERY_KEYS, STORAGE_KEYS } from "../keys";
|
|
11
|
+
import { loadWorkItemOrder, saveWorkItemOrder } from "../repositories/workItemOrder";
|
|
12
|
+
|
|
13
|
+
/** Global (unscoped) hooks
|
|
14
|
+
*
|
|
15
|
+
* Load work item ordering preference for a given instance.
|
|
16
|
+
*
|
|
17
|
+
* @param instanceId The instance id to scope the backlog order to.
|
|
18
|
+
* @returns React Query result containing the backlog order.
|
|
19
|
+
*/
|
|
20
|
+
export function useWorkItemOrderQuery(instanceId: string | null | undefined) {
|
|
21
|
+
const isSharedInstance = !!instanceId;
|
|
22
|
+
|
|
23
|
+
return useQuery({
|
|
24
|
+
queryKey: [QUERY_KEYS.prefs, QUERY_KEYS.backlogOrder, instanceId],
|
|
25
|
+
enabled: isSharedInstance,
|
|
26
|
+
queryFn: () => loadWorkItemOrder(instanceId),
|
|
27
|
+
staleTime: Infinity,
|
|
28
|
+
cacheTime: 1,
|
|
29
|
+
keepPreviousData: false,
|
|
30
|
+
// Keep shared-instance work item order in sync across owners/viewers.
|
|
31
|
+
refetchInterval: isSharedInstance ? 5000 : false,
|
|
32
|
+
refetchOnWindowFocus: isSharedInstance,
|
|
33
|
+
refetchOnReconnect: isSharedInstance,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Save work item ordering preference
|
|
38
|
+
*
|
|
39
|
+
* Persists work item order rows for the given instance (or personal scope),
|
|
40
|
+
* and updates the query cache on success.
|
|
41
|
+
*
|
|
42
|
+
* @param instanceId The instance id or null for personal order.
|
|
43
|
+
* @returns React Query mutation for saving backlog order.
|
|
44
|
+
*/
|
|
45
|
+
export function useSaveWorkItemOrder(instanceId: string | null | undefined) {
|
|
46
|
+
const qc = useQueryClient();
|
|
47
|
+
const effectiveId = instanceId ?? STORAGE_KEYS.personalOrder;
|
|
48
|
+
return useMutation({
|
|
49
|
+
mutationFn: (rows: WorkItemOrderType[]) => saveWorkItemOrder(effectiveId, rows),
|
|
50
|
+
onSuccess: (_, rows) => {
|
|
51
|
+
qc.setQueryData([QUERY_KEYS.prefs, QUERY_KEYS.backlogOrder, effectiveId], rows);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildNormalizedOrder(itemsInCurrentOrder: { id: number }[], stored: WorkItemOrderType[]): WorkItemOrderType[] {
|
|
57
|
+
if (!itemsInCurrentOrder.length) return [];
|
|
58
|
+
|
|
59
|
+
if (!stored || stored.length === 0) {
|
|
60
|
+
// no stored order -> just 1..N
|
|
61
|
+
return itemsInCurrentOrder.map((wi, idx) => ({ id: wi.id, order: idx + 1 }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// index by id for fast lookup
|
|
65
|
+
const byId = new Map(stored.map(r => [r.id, r.order]));
|
|
66
|
+
|
|
67
|
+
// detect if everything looks like "ID as order" or invalid (0/undefined)
|
|
68
|
+
const looksIdBasedOrMissing = stored.length > 0 && stored.every(r =>
|
|
69
|
+
!r.order || r.order === r.id
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (looksIdBasedOrMissing) {
|
|
73
|
+
// treat as no real order -> regenerate 1..N from current order
|
|
74
|
+
return itemsInCurrentOrder.map((wi, idx) => ({ id: wi.id, order: idx + 1 }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// we have at least one “real” order – use it, but normalize and ensure 1..N
|
|
78
|
+
const existingForCurrent = itemsInCurrentOrder
|
|
79
|
+
.map(wi => ({
|
|
80
|
+
id: wi.id,
|
|
81
|
+
order: byId.get(wi.id) ?? Number.MAX_SAFE_INTEGER, // unseen ids go to the end
|
|
82
|
+
}))
|
|
83
|
+
.sort((a, b) => a.order - b.order || a.id - b.id) // stable-ish fallback
|
|
84
|
+
|
|
85
|
+
return existingForCurrent.map((r, idx) => ({ id: r.id, order: idx + 1 }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description: Stores for user preferences using Key-Value Store abstraction.
|
|
3
|
+
* Provides stores for backlog order, column preferences, dialog rows, and saved rows.
|
|
4
|
+
* Utilizes createKVStore to create typed stores for specific storage keys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createKVStore } from "./createStore";
|
|
8
|
+
import { STORAGE_KEYS } from "./keys";
|
|
9
|
+
|
|
10
|
+
export const backlogOrderStore = createKVStore<Array<{ id: number; order: number }>>(STORAGE_KEYS.personalOrder);
|
|
11
|
+
export const columnPrefsStore = createKVStore<{ order: string[]; visible: string[] }>(STORAGE_KEYS.columnPrefs);
|
|
12
|
+
export const dialogRowsStore = createKVStore<any[]>(STORAGE_KEYS.dialogRows);
|
|
13
|
+
export const savedRowsStore = createKVStore<any>(STORAGE_KEYS.savedRows);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description: Stores for user preferences using Key-Value Store abstraction.
|
|
3
|
+
* Provides stores for backlog order, column preferences, dialog rows, and saved rows.
|
|
4
|
+
* Utilizes createKVStore to create typed stores for specific storage keys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ScopeType } from "core/types/ScopeType";
|
|
8
|
+
|
|
9
|
+
export const STORAGE_KEYS = {
|
|
10
|
+
columnPrefs: "crossTeamColumnPrefs",
|
|
11
|
+
dialogRows: "crossTeamDialogRows",
|
|
12
|
+
savedRows: "crossTeamSavedRows",
|
|
13
|
+
crossTeamInstances: "crossTeamInstances",
|
|
14
|
+
crossTeamDefaultInstance: "crossTeamDefaultInstance",
|
|
15
|
+
personalOrder: "personalBacklogOrder",
|
|
16
|
+
personalTaskOrder: "personalTaskOrder"
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const QUERY_KEYS = {
|
|
20
|
+
prefs: "prefs",
|
|
21
|
+
taskOrder: "taskOrder",
|
|
22
|
+
columns: "columns",
|
|
23
|
+
dialowRows: "dialogRows",
|
|
24
|
+
instances: "instances",
|
|
25
|
+
crossSprint: "cross-sprint",
|
|
26
|
+
defaultInstancePref: "defaultInstancePref",
|
|
27
|
+
backlogOrder: "backlogOrder" //workitem order
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SCOPE = process.env.SCOPE as ScopeType | undefined;
|
|
31
|
+
export const DEFAULT_SCOPE: ScopeType = SCOPE || "User";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { CreateInstanceInput } from "core/types/instance/CreateInstanceInputType";
|
|
2
|
+
import type { CrossSprintInstanceMap, CrossSprintInstanceType } from "core/types/instance/CrossSprintInstanceType";
|
|
3
|
+
import { UpdateInstanceInput } from "core/types/instance/UpdateInstanceInputType";
|
|
4
|
+
import type { WorkItemOrderMapType } from "core/types/workItemOrder/WorkItemOrderMapType";
|
|
5
|
+
import { crossTeamInstancesStore, defaultInstanceStore, workItemOrderStore } from "../stores";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load all stored Cross Sprint instances.
|
|
10
|
+
*
|
|
11
|
+
* Reads the instance map from storage and returns it as an array.
|
|
12
|
+
*
|
|
13
|
+
* @returns A list of instances, or an empty array if none exist.
|
|
14
|
+
*/
|
|
15
|
+
export async function loadAllInstances(): Promise<CrossSprintInstanceType[]> {
|
|
16
|
+
const stored = await crossTeamInstancesStore.load();
|
|
17
|
+
|
|
18
|
+
if (!stored) return [];
|
|
19
|
+
|
|
20
|
+
return Object.values(stored);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load the default Cross Sprint instance.
|
|
25
|
+
*
|
|
26
|
+
* Reads the stored default instance preference, then looks up
|
|
27
|
+
* the corresponding instance from all persisted instances.
|
|
28
|
+
*
|
|
29
|
+
* @returns The default Cross Sprint instance if defined and found,
|
|
30
|
+
* otherwise undefined.
|
|
31
|
+
*/
|
|
32
|
+
export async function loadDefaultInstance() {
|
|
33
|
+
const defaultPref = await defaultInstanceStore.load();
|
|
34
|
+
const allInstances = await loadAllInstances();
|
|
35
|
+
|
|
36
|
+
if (!defaultPref?.id) return undefined;
|
|
37
|
+
|
|
38
|
+
return allInstances.find((i) => i.id === defaultPref.id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load a single Cross Sprint instance by id.
|
|
43
|
+
*
|
|
44
|
+
* Reads the instance map from storage, finds the instance,
|
|
45
|
+
* and normalizes it (ensuring `owners` fallback behavior).
|
|
46
|
+
*
|
|
47
|
+
* @param id Instance id to load.
|
|
48
|
+
* @returns The found instance (normalized) or undefined if not found.
|
|
49
|
+
*/
|
|
50
|
+
export async function loadInstanceById(id: string): Promise<CrossSprintInstanceType | undefined> {
|
|
51
|
+
const stored = await crossTeamInstancesStore.load();
|
|
52
|
+
if (!stored) return undefined;
|
|
53
|
+
const inst = stored[id];
|
|
54
|
+
return inst ? normalizeInstance(inst) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create and persist a new Cross Sprint instance.
|
|
59
|
+
*
|
|
60
|
+
* Generates an id, sets timestamps, applies owners fallback,
|
|
61
|
+
* and saves the new instance into the instance map in storage.
|
|
62
|
+
*
|
|
63
|
+
* @param input New instance fields.
|
|
64
|
+
* @returns The created instance object.
|
|
65
|
+
*/
|
|
66
|
+
export async function createCrossSprintInstance(input: CreateInstanceInput): Promise<CrossSprintInstanceType> {
|
|
67
|
+
const { name, description, org, createdBy, owners, projectTeamPairs, } = input;
|
|
68
|
+
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
const id = crypto.randomUUID() ?? Math.random().toString(36).substring(2, 15);
|
|
71
|
+
|
|
72
|
+
const instance: CrossSprintInstanceType = {
|
|
73
|
+
id,
|
|
74
|
+
name,
|
|
75
|
+
description,
|
|
76
|
+
org,
|
|
77
|
+
createdBy,
|
|
78
|
+
owners: owners && owners.length > 0 ? owners : [createdBy],
|
|
79
|
+
projectTeamPairs,
|
|
80
|
+
isDefault: false,
|
|
81
|
+
createdAt: now,
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const stored = await crossTeamInstancesStore.load();
|
|
86
|
+
const current: CrossSprintInstanceMap = stored ?? {};
|
|
87
|
+
|
|
88
|
+
const next: CrossSprintInstanceMap = {
|
|
89
|
+
...current,
|
|
90
|
+
[id]: instance,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
await crossTeamInstancesStore.save(next);
|
|
94
|
+
|
|
95
|
+
return instance;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update an existing Cross Sprint instance.
|
|
100
|
+
*
|
|
101
|
+
* Reads the instance map, finds the target instance, and applies partial updates:
|
|
102
|
+
* - Only provided fields override existing values.
|
|
103
|
+
* - Always updates `updatedAt`.
|
|
104
|
+
*
|
|
105
|
+
* @param input Partial update payload containing the instance id and changed fields.
|
|
106
|
+
* @returns The updated instance, or undefined if storage/instance is missing.
|
|
107
|
+
*/
|
|
108
|
+
export async function updateCrossSprintInstance(input: UpdateInstanceInput): Promise<CrossSprintInstanceType | undefined> {
|
|
109
|
+
const stored = await crossTeamInstancesStore.load();
|
|
110
|
+
if (!stored) return undefined;
|
|
111
|
+
|
|
112
|
+
const { id, name, description, owners, projectTeamPairs, isDefault } = input;
|
|
113
|
+
|
|
114
|
+
const existing = stored[id];
|
|
115
|
+
if (!existing) return undefined;
|
|
116
|
+
|
|
117
|
+
const updated: CrossSprintInstanceType = {
|
|
118
|
+
...existing,
|
|
119
|
+
name: name ?? existing.name,
|
|
120
|
+
description: description ?? existing.description,
|
|
121
|
+
projectTeamPairs: projectTeamPairs ?? existing.projectTeamPairs,
|
|
122
|
+
owners: owners ?? existing.owners,
|
|
123
|
+
isDefault: isDefault ?? existing.isDefault,
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const next: CrossSprintInstanceMap = {
|
|
128
|
+
...stored,
|
|
129
|
+
[id]: updated,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await crossTeamInstancesStore.save(next);
|
|
133
|
+
|
|
134
|
+
return updated;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete a Cross Sprint instance and its related backlog order preferences.
|
|
139
|
+
*
|
|
140
|
+
* Removes the instance entry from the instances map.
|
|
141
|
+
* Also removes any backlog order saved under the same instance id.
|
|
142
|
+
*
|
|
143
|
+
* @param instanceId Instance id to delete.
|
|
144
|
+
* @returns Void.
|
|
145
|
+
*/
|
|
146
|
+
export async function deleteCrossTeamInstance(instanceId: string): Promise<void> {
|
|
147
|
+
|
|
148
|
+
const instances = await crossTeamInstancesStore.load();
|
|
149
|
+
if (instances && typeof instances === "object" && !Array.isArray(instances)) {
|
|
150
|
+
const { [instanceId]: _, ...rest } = instances;
|
|
151
|
+
await crossTeamInstancesStore.save(rest as CrossSprintInstanceMap);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const orders = await workItemOrderStore.load();
|
|
155
|
+
|
|
156
|
+
if (Array.isArray(orders) || !orders) return;
|
|
157
|
+
|
|
158
|
+
const { [instanceId]: __, ...restOrders } = orders as WorkItemOrderMapType;
|
|
159
|
+
await workItemOrderStore.save(restOrders as WorkItemOrderMapType);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
//#region Private Functions
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Normalizes a stored instance into a safe shape for consumers.
|
|
167
|
+
*
|
|
168
|
+
* Ensures `owners` is always present:
|
|
169
|
+
* - If `owners` exists and has items, keep it.
|
|
170
|
+
* - Otherwise fallback to `[createdBy]`.
|
|
171
|
+
*
|
|
172
|
+
* @param inst Raw instance object read from storage.
|
|
173
|
+
* @returns Normalized CrossSprintInstanceType with guaranteed `owners`.
|
|
174
|
+
*/
|
|
175
|
+
function normalizeInstance(inst: any): CrossSprintInstanceType {
|
|
176
|
+
return {
|
|
177
|
+
...inst,
|
|
178
|
+
owners: inst.owners && inst.owners.length > 0
|
|
179
|
+
? inst.owners
|
|
180
|
+
: [inst.createdBy],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TaskOrderMapType } from "core/types/taskOrder/TaskOrderMapType";
|
|
2
|
+
import { TaskOrderType } from "core/types/taskOrder/TaskOrderType";
|
|
3
|
+
import { STORAGE_KEYS } from "../keys";
|
|
4
|
+
import { taskOrderStore } from "../stores";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Save task order for a given instance or personal board.
|
|
8
|
+
*
|
|
9
|
+
* Persists task ordering under an instance-specific key.
|
|
10
|
+
* Falls back to the personal board key when no instance id is provided.
|
|
11
|
+
* Includes backward compatibility for the legacy array-based schema.
|
|
12
|
+
*
|
|
13
|
+
* @param instanceId Instance id or null for personal board.
|
|
14
|
+
* @param rows Ordered tasks to persist.
|
|
15
|
+
*/
|
|
16
|
+
export async function saveTaskOrder(instanceId: string | null | undefined, rows: TaskOrderType[]): Promise<void> {
|
|
17
|
+
const key = instanceId ?? STORAGE_KEYS.personalTaskOrder;
|
|
18
|
+
|
|
19
|
+
const stored = await taskOrderStore.load();
|
|
20
|
+
|
|
21
|
+
// Backward-compat: old schema = plain array
|
|
22
|
+
if (Array.isArray(stored)) {
|
|
23
|
+
const map: TaskOrderMapType = { [key]: rows };
|
|
24
|
+
await taskOrderStore.save(map);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const current: TaskOrderMapType = stored ?? {};
|
|
29
|
+
const next: TaskOrderMapType = {
|
|
30
|
+
...current,
|
|
31
|
+
[key]: rows,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
await taskOrderStore.save(next);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load task order for a given instance or personal board.
|
|
39
|
+
*
|
|
40
|
+
* Resolves task ordering using an instance-specific key.
|
|
41
|
+
* Falls back to the personal board key when no instance id is provided.
|
|
42
|
+
* Includes backward compatibility for the legacy array-based schema.
|
|
43
|
+
*
|
|
44
|
+
* @param instanceId Instance id or null for personal board.
|
|
45
|
+
* @returns Ordered tasks array.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadTaskOrder(instanceId: string | null | undefined): Promise<TaskOrderType[]> {
|
|
48
|
+
const key = instanceId ?? STORAGE_KEYS.personalTaskOrder;
|
|
49
|
+
|
|
50
|
+
const stored = await taskOrderStore.load();
|
|
51
|
+
|
|
52
|
+
// Backward-compat: old schema = plain array -> treat as personal
|
|
53
|
+
if (Array.isArray(stored)) {
|
|
54
|
+
return stored as TaskOrderType[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!stored) return [];
|
|
58
|
+
return stored[key] ?? [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { WorkItemOrderMapType } from "core/types/workItemOrder/WorkItemOrderMapType";
|
|
2
|
+
import { WorkItemOrderType } from "core/types/workItemOrder/WorkItemOrderType";
|
|
3
|
+
import { STORAGE_KEYS } from "../keys";
|
|
4
|
+
import { workItemOrderStore } from "../stores";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Save backlog order for a given instance or personal board.
|
|
8
|
+
*
|
|
9
|
+
* Persists work item ordering under an instance-specific key.
|
|
10
|
+
* Falls back to the personal board key when no instance id is provided.
|
|
11
|
+
* Includes backward compatibility for the legacy array-based schema.
|
|
12
|
+
*
|
|
13
|
+
* @param instanceId Instance id or null for personal board.
|
|
14
|
+
* @param rows Ordered work items to persist.
|
|
15
|
+
*/
|
|
16
|
+
export async function saveWorkItemOrder(instanceId: string | null | undefined, rows: WorkItemOrderType[]): Promise<void> {
|
|
17
|
+
// Fallback key for personal board
|
|
18
|
+
const key = instanceId ?? STORAGE_KEYS.personalOrder;
|
|
19
|
+
|
|
20
|
+
const stored = await workItemOrderStore.load();
|
|
21
|
+
|
|
22
|
+
// Backward-compat: old schema = plain array
|
|
23
|
+
if (Array.isArray(stored)) {
|
|
24
|
+
const map: WorkItemOrderMapType = { [key]: rows };
|
|
25
|
+
await workItemOrderStore.save(map);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const current: WorkItemOrderMapType = stored ?? {};
|
|
30
|
+
const next: WorkItemOrderMapType = {
|
|
31
|
+
...current,
|
|
32
|
+
[key]: rows,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
await workItemOrderStore.save(next);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load backlog order for a given instance or personal board.
|
|
40
|
+
*
|
|
41
|
+
* Resolves work item ordering using an instance-specific key.
|
|
42
|
+
* Falls back to the personal board key when no instance id is provided.
|
|
43
|
+
* Includes backward compatibility for the legacy array-based schema.
|
|
44
|
+
*
|
|
45
|
+
* @param instanceId Instance id or null for personal board.
|
|
46
|
+
* @returns Ordered work items array.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadWorkItemOrder(instanceId: string | null | undefined): Promise<WorkItemOrderType[]> {
|
|
49
|
+
const key = instanceId ?? STORAGE_KEYS.personalOrder;
|
|
50
|
+
|
|
51
|
+
const stored = await workItemOrderStore.load();
|
|
52
|
+
|
|
53
|
+
// Backward-compat: old schema = plain array → treat as personal
|
|
54
|
+
if (Array.isArray(stored)) {
|
|
55
|
+
return stored;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!stored) return [];
|
|
59
|
+
return stored[key] ?? [];
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description: Stores for user preferences using Key-Value Store abstraction.
|
|
3
|
+
* Provides stores for backlog order, column preferences, dialog rows, and saved rows.
|
|
4
|
+
* Utilizes createKVStore to create typed stores for specific storage keys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DefaultInstanceType } from "core/types/instance/DefaultInstanceType";
|
|
8
|
+
import { TaskOrderMapType } from "core/types/taskOrder/TaskOrderMapType";
|
|
9
|
+
import { WorkItemOrderMapType } from "core/types/workItemOrder/WorkItemOrderMapType";
|
|
10
|
+
import { CrossSprintInstanceMap } from "../types/instance/CrossSprintInstanceType";
|
|
11
|
+
import { createKVStore } from "./createStore";
|
|
12
|
+
import { STORAGE_KEYS } from "./keys";
|
|
13
|
+
|
|
14
|
+
//The scope is set as "Default" so all users see same JM-defined order
|
|
15
|
+
export const workItemOrderStore = createKVStore<WorkItemOrderMapType>(STORAGE_KEYS.personalOrder, "Default");
|
|
16
|
+
export const taskOrderStore = createKVStore<TaskOrderMapType>(STORAGE_KEYS.personalTaskOrder, "Default");
|
|
17
|
+
export const crossTeamInstancesStore = createKVStore<CrossSprintInstanceMap>(STORAGE_KEYS.crossTeamInstances, "Default");
|
|
18
|
+
export const defaultInstanceStore = createKVStore<DefaultInstanceType>(STORAGE_KEYS.crossTeamDefaultInstance, "User");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WorkItemRelation } from "azure-devops-extension-api/WorkItemTracking";
|
|
2
|
+
|
|
3
|
+
export type AdoWorkItemType = {
|
|
4
|
+
id: any;
|
|
5
|
+
tempId?: string;
|
|
6
|
+
fields: { [key: string]: any };
|
|
7
|
+
_links?: any;
|
|
8
|
+
url?: string;
|
|
9
|
+
order?: number;
|
|
10
|
+
displayOrder?: string;
|
|
11
|
+
rev?: number;
|
|
12
|
+
multiLineFields?: { [k: string]: string[] };
|
|
13
|
+
relations?: WorkItemRelation[];
|
|
14
|
+
isNewTask?: boolean;
|
|
15
|
+
parentWorkItem?: AdoWorkItemType;
|
|
16
|
+
isFaded?: boolean | undefined;
|
|
17
|
+
_children?: AdoWorkItemType[];
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type KVStore<T> = { load(): Promise<T | undefined>; save(value: T): Promise<void>; };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ScopeType = "User" | "Default";
|
package/@danieli-automation/devops-plugin-core/src/core/types/instance/CreateInstanceInputType.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SelectedProjectType } from "core/types/SelectedProjectType";
|
|
2
|
+
|
|
3
|
+
export type CreateInstanceInput = {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
org: string;
|
|
7
|
+
createdBy: string; // current user id/descriptor
|
|
8
|
+
owners?: string[]; // if omitted, default [createdBy]
|
|
9
|
+
projectTeamPairs: SelectedProjectType[];
|
|
10
|
+
};
|
package/@danieli-automation/devops-plugin-core/src/core/types/instance/CrossSprintInstanceType.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SelectedProjectType } from "core/types/SelectedProjectType";
|
|
2
|
+
|
|
3
|
+
export type CrossSprintInstanceType = {
|
|
4
|
+
id: string; // GUID/UUID
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
|
|
8
|
+
org: string; // dev.azure.com/{org}
|
|
9
|
+
createdBy: any; // user identifier (UPN/email or descriptor)
|
|
10
|
+
owners: string[]; // list of JMs (can contain createdBy)
|
|
11
|
+
|
|
12
|
+
projectTeamPairs: SelectedProjectType[]; // N project/team pairs
|
|
13
|
+
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
|
|
17
|
+
isDefault?: boolean; // whether this instance is the default one for the whole TEAM
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CrossSprintInstanceMap = Record<string, CrossSprintInstanceType>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DefaultInstanceType = {
|
|
2
|
+
id?: string; // GUID/UUID
|
|
3
|
+
name?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
|
|
6
|
+
org?: string; // dev.azure.com/{org}
|
|
7
|
+
createdBy?: string; // user identifier (UPN/email or descriptor)
|
|
8
|
+
owners?: string[]; // list of JMs (can contain createdBy)
|
|
9
|
+
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
updatedAt?: string;
|
|
12
|
+
}
|