Skip to content

Commit

Permalink
✨ Enable Dynamic Mappings in the Control (#65)
Browse files Browse the repository at this point in the history
✨ (WorkItemCloneCommand): add support for dynamic field value processing
📝 (README.md): update documentation to reflect new field processing
features

Introduce a new feature to dynamically process field values using
template and control data. This allows more flexible and powerful field
value assignments in the control file. The README.md is updated to
document the new syntax and usage examples, ensuring users understand
how to leverage these new capabilities. This change enhances the tool's
functionality by allowing more complex and dynamic work item creation
scenarios.
  • Loading branch information
MrHinsh authored Jul 29, 2024
2 parents 0bc3778 + f9d09c1 commit 0b09b0f
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 23 deletions.
1 change: 1 addition & 0 deletions AzureDevOps-workitem-clone.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
TestData\ADO_TESTProjPipline_V03.json = TestData\ADO_TESTProjPipline_V03.json
GitVersion.yml = GitVersion.yml
.github\workflows\main.yml = .github\workflows\main.yml
README.md = README.md
EndProjectSection
EndProject
Global
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading.Tasks;
using System.Diagnostics.Eventing.Reader;
using AzureDevOps.WorkItemClone.Repositories;
using System.Text.RegularExpressions;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
Expand Down Expand Up @@ -267,31 +268,80 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildList(JAr
newItem.templateId = jsonItemTemplateId;
newItem.workItemType = templateWorkItem != null ? templateWorkItem.fields["System.WorkItemType"].ToString() : "Deliverable";
newItem.fields = new Dictionary<string, string>();
var fields = controlItem["fields"].ToObject<Dictionary<string, string>>();
foreach (var field in fields)
var controlFields = controlItem["fields"].ToObject<Dictionary<string, string>>();
foreach (var field in controlFields)
{
switch (field.Key)
var fieldKey = field.Key;
var fieldValue = field.Value;
fieldValue = ProcessFieldValue(templateWorkItem, controlFields, field, fieldKey, fieldValue);
switch (fieldKey)
{
case "System.AreaPath":
newItem.fields.Add(field.Key, string.Join("\\", targetTeamProject, field.Value));
newItem.fields.Add(fieldKey, string.Join("\\", targetTeamProject, field.Value));
break;
default:
if (templateWorkItem != null && templateWorkItem.fields.ContainsKey(field.Key) && (field.Value.Contains("${valuefromtemplate}") || field.Value.Contains("${fromtemplate}")))
newItem.fields.Add(fieldKey, fieldValue);
break;
}
}
newItem.relations = new List<WorkItemToBuildRelation>() { new WorkItemToBuildRelation() { rel = "System.LinkTypes.Hierarchy-Reverse", targetId = projectItem.id } };
yield return newItem;
}
}

private static string ProcessFieldValue(WorkItemFull templateWorkItem, Dictionary<string, string>? controlFields, KeyValuePair<string, string> field, string fieldKey, string fieldValue)
{
var fieldMatches = Regex.Matches(field.Value, @"\$\[([^\|\]]+)(?:\|([^\]]+))?\]");
if (templateWorkItem != null)
{
// Check and replace all template field names
foreach (Match match in fieldMatches)
{
var key = match.Groups[1].Value;
var option = match.Groups[2].Value;
switch (option)
{
case "control":
if (controlFields.ContainsKey(key))
{
/// Add the value from the template
newItem.fields.Add(field.Key, templateWorkItem.fields[field.Key].ToString());
fieldValue = fieldValue.Replace(match.Value, controlFields[key].ToString());
}
else
break;
case "template":
default:
if (templateWorkItem.fields.ContainsKey(key))
{
/// add value from control file
newItem.fields.Add(field.Key, field.Value);
fieldValue = fieldValue.Replace(match.Value, templateWorkItem.fields[key].ToString());
}
break;
}
}
newItem.relations = new List<WorkItemToBuildRelation>() { new WorkItemToBuildRelation() { rel = "System.LinkTypes.Hierarchy-Reverse", targetId = projectItem.id } };
yield return newItem;
// Check and replace old school replace flags
if (templateWorkItem.fields.ContainsKey(fieldKey) && (fieldValue.Contains("${valuefromtemplate}") || fieldValue.Contains("${fromtemplate}")))
{
fieldValue = field.Value
.Replace("${valuefromtemplate}", templateWorkItem.fields[fieldKey].ToString())
.Replace("${fromtemplate}", templateWorkItem.fields[fieldKey].ToString());
}
}
else
{
// Check and replace all template field names
foreach (Match match in fieldMatches)
{
var key = match.Groups[1].Value;
if (templateWorkItem.fields.ContainsKey(key))
{
fieldValue = fieldValue.Replace($"$[{key}]", "");
}
}
// Ensure we remove the template values
fieldValue = field.Value
.Replace("${valuefromtemplate}", "")
.Replace("${fromtemplate}", "");
}

return fieldValue;
}

private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildRelations(List<WorkItemToBuild> workItemsToBuild, List<WorkItemFull> templateWorkItems)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings
[DefaultValue("./cache")]
public string? CachePath { get; set; }
//------------------------------------------------
[CommandOption("--jsonFile|--inputJsonFile")]
[CommandOption("--jsonFile|--inputJsonFile|--controlFile")]
public string? inputJsonFile { get; set; }
//------------------------------------------------
[Description("The access token for the target location")]
Expand Down
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,15 @@ Clones work items from a template project to a target project incorproating a JS
}
```

## Json Input File
## Control File (--inputFile)

The control file consists of a list of work items that you want to create. Each work item has an `id` and a list of `fields`.

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.
- `id` - The `id` is the optional ID of a template item. If there is an ID, and there is a template item, then the tool will load that template and allow you to select values from it to add to the work item that you create. This template item will also be used to load relationships to other work items and if both ends of the relationship are part of the control file it will wire them up as expected. Not specifying an D wil result in a new work item based only on the control file.
- `fields` - These `fields` are the fields that will be used to create the work item. You can use any field `Refname` from the target Azure DevOps work item.

Use the `${fromtemplate}` to specify that the value should be taken from the template. This is used here for the Description and Acceptance Criteria, but can be used to pull data from any field.
example:

```json
[
Expand All @@ -129,9 +130,7 @@ Use the `${fromtemplate}` to specify that the value should be taken from the tem
"System.Title": "Technical specification",
"Custom.Product": "CC",
"Microsoft.VSTS.Scheduling.Effort": 12,
"Custom.TRA_Milestone": "E0.1",
"System.Description": "${fromtemplate}",
"Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}"
"Custom.TRA_Milestone": "E0.1"
}
},
{
Expand All @@ -143,9 +142,54 @@ Use the `${fromtemplate}` to specify that the value should be taken from the tem
"Custom.Product": "",
"Microsoft.VSTS.Scheduling.Effort": 2,
"Custom.TRA_Milestone": "E4.8"
"System.Description": "${fromtemplate}",
}
]
```

### Fields Manipulation

If you wish to pull fields from the template work item you can use the following syntax:

- `$[System.Description|template]` - this will pull the `System.Description` field from the template work item. Since template is the default you can also use `$[System.Description]`.
- `$[System.Description|control]` - this will pull the `System.Description` field from the control item data.

This would make the following valid:

```json
[
{
"id": 213928,
"fields": {
"System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1",
"System.Tags": "Customer Document",
"System.Title": "Technical specification [ $[Custom.Product|control] ]",
"Custom.Product": "CC",
"Microsoft.VSTS.Scheduling.Effort": 12,
"Custom.TRA_Milestone": "E0.1",
"System.Description": "$[System.Description] for $[Custom.Product|control]",
"Microsoft.VSTS.Common.AcceptanceCriteria": "$[Microsoft.VSTS.Common.AcceptanceCriteria]"
}
}
]
```

You can also specify `${fromtemplate}` or `${valuefromtemplate}` to pull just the value from the template that is the same name as the control field in focus.

```json
[
{
"id": 213928,
"fields": {
"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",
"System.Description": "The description: ${fromtemplate}",
"Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}"
}
}
]
```

18 changes: 16 additions & 2 deletions TestData/tst_jsonj_export_v20-lite.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,22 @@
"Custom.Product": "CC",
"Microsoft.VSTS.Scheduling.Effort": 12,
"Custom.TRA_Milestone": "E0.1",
"System.Description": "${fromtemplate}",
"Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}"
"System.Description": "here is the descrption: ${fromtemplate}",
"Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}",
"Custom.CombineDA": "<h1>Description></h1>$[System.Description]<br /><h2>Acceptance</h2>$[Microsoft.VSTS.Common.AcceptanceCriteria]<h2>Entra</h2>$[Custom.Product|control]"
}
},
{
"id": 213928,
"fields": {
"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",
"System.Description": "here is the descrption: $[System.Description] for $[Custom.Product|control]",
"Microsoft.VSTS.Common.AcceptanceCriteria": "$[Microsoft.VSTS.Common.AcceptanceCriteria]"
}
}
]

0 comments on commit 0b09b0f

Please sign in to comment.