diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs index c173b29..028dd43 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs @@ -8,6 +8,7 @@ using Microsoft.Azure.Pipelines.WebApi; using Microsoft.VisualStudio.Services.CircuitBreaker; using Newtonsoft.Json.Linq; +using System.Threading.Tasks; using System.Diagnostics.Eventing.Reader; using AzureDevOps.WorkItemClone.Repositories; @@ -75,6 +76,7 @@ await AnsiConsole.Progress() var task5 = ctx.AddTask("[bold]Stage 5[/]: Create Output Plan Relations ", false); //var task51 = ctx.AddTask("[bold]Stage 5.1[/]: Validate Data ", false); var task6 = ctx.AddTask("[bold]Stage 6[/]: Create Work Items", false); + var task7 = ctx.AddTask("[bold]Stage 7[/]: Create Query", false); string cacheTemplateWorkItemsFile = $"{config.CachePath}\\templateCache-{config.templateOrganization}-{config.templateProject}-{config.templateParentId}.json"; @@ -218,8 +220,30 @@ await AnsiConsole.Progress() } task6.StopTask(); //AnsiConsole.WriteLine($"Stage 6: All Work Items Created."); + + + // -------------------------------------------------------------- + // Task 7: Create Query + task7.MaxValue = 1; + task7.StartTask(); + + Dictionary queryParameters = new Dictionary() + { + { "@projectID", projectItem.id.ToString() }, + { "@projectTitle", projectItem.fields.SystemTitle }, + { "@projectTags", projectItem.fields.SystemTags }, + { "@RunName", config.RunName } + }; + var query = await targetApi.CreateProjectQuery(config.targetQueryTitle, config.targetQuery, queryParameters); + task7.Increment(1); + task7.StopTask(); + + + }); + + AnsiConsole.WriteLine($"Complete..."); diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs index 8e1434c..4c8b07e 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs @@ -42,6 +42,23 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings [CommandOption("--targetFalbackWit")] [DefaultValue("Deliverable")] public string? targetFalbackWit { get; set; } + + + [Description("The WIQL Query to use. You can use @projectID, @projectTitle, @projectTags to replace data from the project!")] + [CommandOption("--targetQuery")] + [DefaultValue("SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID")] + public string? targetQuery { get; set; } + + [Description("The title to use for the query. You can use @projectID, @projectTitle, @projectTags, @RunName to replace data from the project!")] + [CommandOption("--targetQueryTitle")] + [DefaultValue("Project-@RunName - @projectTitle")] + public string? targetQueryTitle { get; set; } + + [Description("Must already Exist and be in the form 'Shared Queries/Folder1/Folder2'!")] + [CommandOption("--targetQueryFolder")] + [DefaultValue("Shared Queries")] + public string? targetQueryFolder { get; set; } + //------------------------------------------------ [Description("The access token for the template location")] [CommandOption("--templateAccessToken")] diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs index 74639e0..303ab8d 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs @@ -37,6 +37,9 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se config.targetParentId = EnsureIntAskIfMissing(config.targetParentId = settings.targetParentId != null ? settings.targetParentId : config.targetParentId, "Provide the target parent?"); config.targetFalbackWit = EnsureStringAskIfMissing(config.targetFalbackWit = settings.targetFalbackWit != null ? settings.targetFalbackWit : config.targetFalbackWit, "Provide the target fallback wit?"); + config.targetQueryTitle = EnsureStringAskIfMissing(config.targetQueryTitle = settings.targetQueryTitle != null ? settings.targetQueryTitle : config.targetQueryTitle, "Provide the target query title?"); + config.targetQueryFolder = EnsureStringAskIfMissing(config.targetQueryFolder = settings.targetQueryFolder != null ? settings.targetQueryFolder : config.targetQueryFolder, "Provide the target query folder?"); + config.targetQuery = EnsureStringAskIfMissing(config.targetQuery = settings.targetQuery != null ? settings.targetQuery : config.targetQuery, "Provide the target WIQL query?"); } diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json index 1fc1ff2..749472e 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json +++ b/AzureDevOps.WorkItemClone.ConsoleUI/configuration.json @@ -8,5 +8,9 @@ "templateAccessToken": null, "templateOrganization": "ABB-MO-ATE", "templateProject": "ABB Traction Template", - "templateParentId": 212315 + "templateParentId": 212315, + "targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID", + "targetQueryTitle": "Project-@RunName - @projectTitle", + "targetQueryFolder": "Shared Queries" + } diff --git a/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs b/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs index 1d59a43..26826d5 100644 --- a/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs +++ b/AzureDevOps.WorkItemClone/AzureDevOpsApi.cs @@ -51,6 +51,18 @@ public async IAsyncEnumerable GetWorkItemsFullAsync(Workitem[] ite } public async Task GetWiqlQueryResults(string wiqlQuery, Dictionary parameters) + { + wiqlQuery = GetQueryString(wiqlQuery, parameters); + string post = JsonConvert.SerializeObject(new + { + query = wiqlQuery + }); + string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2"; + var result = await GetObjectResult(apiCallUrl, post); + return result.result; + } + + private string GetQueryString( string wiqlQuery, Dictionary parameters) { if (parameters == null) { @@ -64,19 +76,23 @@ public async IAsyncEnumerable GetWorkItemsFullAsync(Workitem[] ite { wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems Where [System.TeamProject] = '@project' order by [System.CreatedDate] desc"; } - foreach (var param in parameters) + wiqlQuery = ReplaceParamsInString(wiqlQuery, parameters); + return wiqlQuery; + } + private string ReplaceParamsInString(string text, Dictionary parameters) + { + if (string.IsNullOrEmpty(text)) { - wiqlQuery = wiqlQuery.Replace(param.Key, param.Value); + text = "Default"; } - string post = JsonConvert.SerializeObject(new + foreach (var param in parameters) { - query = wiqlQuery - }); - string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2"; - var result = await GetObjectResult(apiCallUrl, post); - return result.result; + text = text.Replace(param.Key, param.Value); + } + return text; } + public async Task GetWiqlQueryResults() { string post = JsonConvert.SerializeObject(new { @@ -230,6 +246,23 @@ public ValueTask DisposeAsync() { return new(Task.Delay(TimeSpan.FromSeconds(1))); } + + public async Task CreateProjectQuery(string queryName, string wiqlQuery, Dictionary parameters) + { + ///POST https://dev.azure.com/{organization}/{project}/_apis/wit/queries/{query}?api-version=7.1-preview.2 + wiqlQuery = GetQueryString(wiqlQuery, parameters); + queryName = ReplaceParamsInString(queryName, parameters); + string post = JsonConvert.SerializeObject(new + { + isFolder = false, + name = queryName, + path = $"Shared Queries/{queryName}", + wiql = wiqlQuery + }); + string apiCallUrl = $"https://dev.azure.com/{_account}/{_project}/_apis/wit/queries/Shared Queries/?api-version=7.2-preview.2"; + var result = await GetObjectResult(apiCallUrl, post); + return result.result; + } } } diff --git a/AzureDevOps.WorkItemClone/DataContracts/Queries.cs b/AzureDevOps.WorkItemClone/DataContracts/Queries.cs new file mode 100644 index 0000000..7caa8dc --- /dev/null +++ b/AzureDevOps.WorkItemClone/DataContracts/Queries.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AzureDevOps.WorkItemClone.DataContracts +{ + + public class Query + { + public string id { get; set; } + public string name { get; set; } + public string path { get; set; } + public QueryCreatedby createdBy { get; set; } + public DateTime createdDate { get; set; } + public QueryLastmodifiedby lastModifiedBy { get; set; } + public DateTime lastModifiedDate { get; set; } + public bool isFolder { get; set; } + public bool hasChildren { get; set; } + public bool isPublic { get; set; } + public QueryLinks2 _links { get; set; } + public string url { get; set; } + } + + public class QueryCreatedby + { + public string displayName { get; set; } + public string url { get; set; } + public QueryLinks _links { get; set; } + public string id { get; set; } + public string uniqueName { get; set; } + public string imageUrl { get; set; } + public string descriptor { get; set; } + } + + public class QueryLinks + { + public Avatar avatar { get; set; } + } + + public class QueryAvatar + { + public string href { get; set; } + } + + public class QueryLastmodifiedby + { + public string displayName { get; set; } + public string url { get; set; } + public QueryLinks1 _links { get; set; } + public string id { get; set; } + public string uniqueName { get; set; } + public string imageUrl { get; set; } + public string descriptor { get; set; } + } + + public class QueryLinks1 + { + public QueryAvatar1 avatar { get; set; } + } + + public class QueryAvatar1 + { + public string href { get; set; } + } + + public class QueryLinks2 + { + public QuerySelf self { get; set; } + public QueryHtml html { get; set; } + public QueryParent parent { get; set; } + } + + public class QuerySelf + { + public string href { get; set; } + } + + public class QueryHtml + { + public string href { get; set; } + } + + public class QueryParent + { + public string href { get; set; } + } + +} diff --git a/README.md b/README.md index 9c0c5c0..6f9ffff 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,19 @@ Clones work items from a template project to a target project incorproating a JS - `--targetProject` - The name of the prject to clone work items to. - `--targetParentId` - All cloned work items will be come a child of this work item + *Target Query* - The target query is used to create a query in the target project to show the cloned work items. + + - `--targetQuery` - The query to create in the target project. Default is `SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID`. + - `--targetQueryTitle` - The title of the query to create in the target project. Default is `Project-@RunName - @projectTitle`. + - `--targetQueryFolder` - The folder to create the query in the target project. Default is `Shared Queries`. + + You can use the following parameters: + + - *@projectID* - The ID of the target parent item + - *@projectTitle* - The title of the parent item + - *@projectTags* - the tags of the item + - *@RunName* - The name of the run + *Optional Parameters* - These are optional parameters that can be used to control the behaviour of the clone process. - `--NonInteractive` - Disables interactive mode. Default is `false`. @@ -61,7 +74,6 @@ Clones work items from a template project to a target project incorproating a JS ``` - ### `init` Leads you through the process of creating a configuration file. @@ -83,66 +95,51 @@ Clones work items from a template project to a target project incorproating a JS ```json { "CachePath": "./cache", - "inputJsonFile": "ADO_TESTProjPipline_V03.json", - "targetAccessToken": null, + "inputJsonFile": "TESTProjPipline_V03.json", + "targetAccessToken": "************************************", "targetOrganization": "nkdagility-preview", "targetProject": "Clone-Demo", "targetParentId": 540, - "templateAccessToken": null, - "templateOrganization": "Clone-MO-ATE", - "templateProject": "Clone Template" + "templateAccessToken": "************************************", + "templateOrganization": "orgname", + "templateProject": "template Project", + "templateParentId": 212315, + "targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID", + "targetQueryTitle": "Project-@RunName - @projectTitle", + "targetQueryFolder": "Shared Queries" } ``` - ## inputJsonFile Example +## Json Input File + + +The `id` is the ID of the template item. This will be used to specifiy Description, Acceptance Criteria, and dependancy relationsips. I the `id` is not specified a new work item will be created. + +The `fields` are the fields that will be used to create the work item. You can use any field ientifyer from Azure DevOps. ```json - [ +[ { "id": 213928, - "area": "TPL", - "tags": "Customer Document", "fields": { - "title": "Technical specification", - "product": "CC000_000A01" + "System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1", + "System.Tags": "Customer Document", + "System.Title": "Technical specification", + "Custom.Product": "CC", + "Microsoft.VSTS.Scheduling.Effort": 12, + "Custom.TRA_Milestone": "E0.1" } }, { - "id": 213928, - "area": "TPL", - "tags": "Customer Document", + "id": "", "fields": { - "title": "Technical specification", - "product": "CC000_000A02" + "System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1", + "System.Tags": "", + "System.Title": "E4.8 Assessment", + "Custom.Product": "", + "Microsoft.VSTS.Scheduling.Effort": 2, + "Custom.TRA_Milestone": "E4.8" } - } -] -``` - -proposed new format not yet adopted: - - - ```json - [ - { - "templateId": 213928, - "fields": [ - {"System.Title": "Technical specification"}, - {"Custom.Project": "CC000_000A01"}, - {"System.Tags": "Customer Document"}, - {"System.AreaPath": "#{targetProject}#\\TPL"} - ] - }, - { - "templateId": 213928, - "fields": [ - {"System.Title": "Technical specification"}, - {"Custom.Project": "CC000_000A02"}, - {"System.Tags": "Technical specification"}, - {"System.AreaPath": "#{targetProject}#\\TPL"} - ] - }, - } ] ```