From 8d5e33f7c6a7015c2e161d5139933b4eb59f964c Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Sun, 28 May 2023 22:09:12 +0200 Subject: [PATCH 01/19] function Get-VSTeamWorkItemRelationType --- .docs/Get-VSTeamWorkItemRelationType.md | 49 ++++ .../Get-VSTeamWorkItemRelationType.md | 1 + Source/Private/applyTypes.ps1 | 7 + .../Public/Get-VSTeamWorkItemRelationType.ps1 | 24 ++ ..._lib.WorkItemRelationType.TableView.ps1xml | 45 +++ .../vsteam_lib.WorkItemRelationType.ps1xml | 30 ++ .../Get-VSTeamWorkItemRelationType.json | 273 ++++++++++++++++++ .../Get-VSTeamWorkItemRelationType.Tests.ps1 | 31 ++ config.json | 3 +- 9 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 .docs/Get-VSTeamWorkItemRelationType.md create mode 100644 .docs/synopsis/Get-VSTeamWorkItemRelationType.md create mode 100644 Source/Public/Get-VSTeamWorkItemRelationType.ps1 create mode 100644 Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml create mode 100644 Source/types/vsteam_lib.WorkItemRelationType.ps1xml create mode 100644 Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json create mode 100644 Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 diff --git a/.docs/Get-VSTeamWorkItemRelationType.md b/.docs/Get-VSTeamWorkItemRelationType.md new file mode 100644 index 000000000..c50294b3d --- /dev/null +++ b/.docs/Get-VSTeamWorkItemRelationType.md @@ -0,0 +1,49 @@ + + +# Get-VSTeamWorkItemType + +## SYNOPSIS + + + +## SYNTAX + +## Description + + + +## EXAMPLES + +### Example 1 + +```powershell +Get-VSTeamWorkItemRelationType -Usage WorkItemLink +``` + +This command gets a single work item type. + +## PARAMETERS + +### Usage +Usage of the relations. The acceptable values for this parameter are (Default value is WorkItemLink): +- All +- ResourceLink +- WorkItemLink + +```yaml +Type: String +``` + + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + + + + +## RELATED LINKS diff --git a/.docs/synopsis/Get-VSTeamWorkItemRelationType.md b/.docs/synopsis/Get-VSTeamWorkItemRelationType.md new file mode 100644 index 000000000..b998c8047 --- /dev/null +++ b/.docs/synopsis/Get-VSTeamWorkItemRelationType.md @@ -0,0 +1 @@ +Returns a list of the different relation types between work items and links inside the same work item \ No newline at end of file diff --git a/Source/Private/applyTypes.ps1 b/Source/Private/applyTypes.ps1 index 96f30cbd5..e39474148 100644 --- a/Source/Private/applyTypes.ps1 +++ b/Source/Private/applyTypes.ps1 @@ -187,4 +187,11 @@ function _applyTypesToAgentPoolMaintenance { [CmdletBinding()] param($item) $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.AgentPoolMaintenance') +} + +function _applyTypesToWorkItemRelationType { + [CmdletBinding()] + param ($item) + + $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.WorkItemRelationType') } \ No newline at end of file diff --git a/Source/Public/Get-VSTeamWorkItemRelationType.ps1 b/Source/Public/Get-VSTeamWorkItemRelationType.ps1 new file mode 100644 index 000000000..9f168a06d --- /dev/null +++ b/Source/Public/Get-VSTeamWorkItemRelationType.ps1 @@ -0,0 +1,24 @@ +function Get-VSTeamWorkItemRelationType { + [CmdletBinding(HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Get-VSTeamWorkItemRelationType')] + param ( + [ValidateSet('All', 'ResourceLink', 'WorkItemLink')] + [string]$Usage = 'WorkItemLink' + ) + + process { + $commonArgs = @{ + area = 'wit' + resource = 'workitemrelationtypes' + version = $(_getApiVersion Core) + noProject = $true + } + $resp = _callAPI @commonArgs + foreach ($item in $resp.value) { + if ($Usage -eq 'All' -or $Usage -eq $item.attributes.usage) { + _applyTypesToWorkItemRelationType -item $item + Write-Output $item + } + } + } + +} \ No newline at end of file diff --git a/Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml b/Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml new file mode 100644 index 000000000..48f461caf --- /dev/null +++ b/Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml @@ -0,0 +1,45 @@ + + + + + vsteam_lib.WorkItemRelationType.TableView + + vsteam_lib.WorkItemRelationType + + + + + + + + + + + + + + + + + + + + + Name + + + ReferenceName + + + Usage + + + Topology + + + + + + + + \ No newline at end of file diff --git a/Source/types/vsteam_lib.WorkItemRelationType.ps1xml b/Source/types/vsteam_lib.WorkItemRelationType.ps1xml new file mode 100644 index 000000000..14ba046f4 --- /dev/null +++ b/Source/types/vsteam_lib.WorkItemRelationType.ps1xml @@ -0,0 +1,30 @@ + + + + vsteam_lib.WorkItemRelationType + + + Usage + $this.attributes.usage + + + Topology + $this.attributes.topology + + + PSStandardMembers + + + DefaultDisplayPropertySet + + name + referenceName + usage + Topology + + + + + + + \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json b/Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json new file mode 100644 index 000000000..23cc5de54 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json @@ -0,0 +1,273 @@ +{ + "count": 18, + "value": [ + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.Affects-Reverse" + }, + "referenceName": "Microsoft.VSTS.Common.Affects-Forward", + "name": "Affects", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.Affects-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.Affects-Forward" + }, + "referenceName": "Microsoft.VSTS.Common.Affects-Reverse", + "name": "Affected By", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.Affects-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward", + "name": "Referenced By", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse", + "name": "References", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.TestedBy-Reverse" + }, + "referenceName": "Microsoft.VSTS.Common.TestedBy-Forward", + "name": "Tested By", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.TestedBy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.TestedBy-Forward" + }, + "referenceName": "Microsoft.VSTS.Common.TestedBy-Reverse", + "name": "Tests", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.TestedBy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward", + "name": "Test Case", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse", + "name": "Shared Steps", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": true, + "oppositeEndReferenceName": "System.LinkTypes.Duplicate-Reverse" + }, + "referenceName": "System.LinkTypes.Duplicate-Forward", + "name": "Duplicate", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Duplicate-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": false, + "oppositeEndReferenceName": "System.LinkTypes.Duplicate-Forward" + }, + "referenceName": "System.LinkTypes.Duplicate-Reverse", + "name": "Duplicate Of", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Duplicate-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "System.LinkTypes.Dependency-Reverse" + }, + "referenceName": "System.LinkTypes.Dependency-Forward", + "name": "Successor", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Dependency-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "System.LinkTypes.Dependency-Forward" + }, + "referenceName": "System.LinkTypes.Dependency-Reverse", + "name": "Predecessor", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Dependency-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": true, + "oppositeEndReferenceName": "System.LinkTypes.Hierarchy-Reverse" + }, + "referenceName": "System.LinkTypes.Hierarchy-Forward", + "name": "Child", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Hierarchy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": false, + "oppositeEndReferenceName": "System.LinkTypes.Hierarchy-Forward" + }, + "referenceName": "System.LinkTypes.Hierarchy-Reverse", + "name": "Parent", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Hierarchy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": false, + "directional": false, + "singleTarget": true, + "topology": "network" + }, + "referenceName": "System.LinkTypes.Related", + "name": "Related", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Related" + }, + { + "attributes": { + "usage": "resourceLink", + "editable": false, + "enabled": true + }, + "referenceName": "AttachedFile", + "name": "Attached File", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/AttachedFile" + }, + { + "attributes": { + "usage": "resourceLink", + "editable": false, + "enabled": true + }, + "referenceName": "Hyperlink", + "name": "Hyperlink", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Hyperlink" + }, + { + "attributes": { + "usage": "resourceLink", + "editable": false, + "enabled": true + }, + "referenceName": "ArtifactLink", + "name": "Artifact Link", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/ArtifactLink" + } + ] + } \ No newline at end of file diff --git a/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 new file mode 100644 index 000000000..679861dd4 --- /dev/null +++ b/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 @@ -0,0 +1,31 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelationType' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItemRelationType.json' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + } + + Context 'Get-VSTeamWorkItemRelationType' { + It 'Should return relations default to WorkItemLink' { + ## Act + Get-VSTeamWorkItemRelationType + + ## Assert + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitemrelationtypes?api-version=$(_getApiVersion Core)" + } + } + + It 'With All parameter should return 2 relation types' { + ## Act + $relationTypes = Get-VSTeamWorkItemRelationType -Usage All + + ## Assert + $relationTypes | Select-Object Usage -Unique | Should -HaveCount 2 + } + } +} \ No newline at end of file diff --git a/config.json b/config.json index 17643213e..6b14dc454 100644 --- a/config.json +++ b/config.json @@ -128,7 +128,8 @@ "vsteam_lib.Build.ListView.ps1xml", "vsteam_lib.ResourceArea.TableView.ps1xml", "vsteam_lib.PullRequest.TableView.ps1xml", - "vsteam_lib.Policy.TableView.ps1xml" + "vsteam_lib.Policy.TableView.ps1xml", + "vsteam_lib.WorkItemRelationType.TableView.ps1xml" ] } } \ No newline at end of file From 9bcce77fc25d81c0a78a8e2f9d57fe4e66217b4e Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Sat, 17 Jun 2023 19:21:43 +0200 Subject: [PATCH 02/19] New-VSTeamWorkItemRelation --- .docs/New-VSTeamWorkItemRelation.md | 152 ++++++++++++++++++ .docs/synopsis/New-VSTeamWorkItemRelation.md | 1 + .../RelationTypeToReferenceNameAttribute.cs | 19 +++ Source/Classes/Cache/RelationTypeCache.cs | 69 ++++++++ .../WorkItemRelationTypeCompleter.cs | 37 +++++ Source/Private/applyTypes.ps1 | 7 + Source/Public/New-VSTeamWorkItemRelation.ps1 | 48 ++++++ ...team_lib.WorkItemRelation.TableView.ps1xml | 45 ++++++ ...et-VSTeamWorkItemRelationTypePSObject.json | 110 +++++++++++++ .../New-VSTeamWorkItemRelation.Tests.ps1 | 80 +++++++++ ...lationTypeToReferenceNameAttributeTests.cs | 37 +++++ Tests/library/Cache/RelationTypeCacheTests.cs | 145 +++++++++++++++++ .../WorkItemRelationTypeCompleterTest.cs | 60 +++++++ config.json | 3 +- 14 files changed, 812 insertions(+), 1 deletion(-) create mode 100644 .docs/New-VSTeamWorkItemRelation.md create mode 100644 .docs/synopsis/New-VSTeamWorkItemRelation.md create mode 100644 Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs create mode 100644 Source/Classes/Cache/RelationTypeCache.cs create mode 100644 Source/Classes/Completer/WorkItemRelationTypeCompleter.cs create mode 100644 Source/Public/New-VSTeamWorkItemRelation.ps1 create mode 100644 Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml create mode 100644 Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json create mode 100644 Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 create mode 100644 Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs create mode 100644 Tests/library/Cache/RelationTypeCacheTests.cs create mode 100644 Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs diff --git a/.docs/New-VSTeamWorkItemRelation.md b/.docs/New-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..552584ff0 --- /dev/null +++ b/.docs/New-VSTeamWorkItemRelation.md @@ -0,0 +1,152 @@ + + +# New-VSTeamWorkItemRelation + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### Example 1 + +```powershell +New-VSTeamWorkItemRelation -RelationType Duplicate -Id 55 -Operation Remove -Comment "not needed" + +Id RelationType Operation Comment +-- ------------ --------- ------- +55 System.LinkTypes.Duplicate-Forward remove not needed +``` + +Simple invocation, returns a Relation object. + + +### Example 2 + +```powershell +$relations = New-VSTeamWorkItemRelation -RelationType Related -Operation Remove -Id 55 | + New-VSTeamWorkItemRelation -RelationType Related -Operation Remove -Id 66 | + New-VSTeamWorkItemRelation -RelationType Related -Operation Remove -Id 77 | +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Removes work items 55, 66 and 77 relations from work item 30 + +### Example 3 + +```powershell +$relations =@() +$relations += New-VSTeamWorkItemRelation -RelationType Related -Id 55 -Operation Remove +$relations += New-VSTeamWorkItemRelation -RelationType Related -Id 66 -Operation Add +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Similar to example 2, but managing the relations collection directly + +### Example 4 + +```powershell +$relations = 55,66 | New-VSTeamWorkItemRelation -RelationType Child -Operation Add +Update-VSTeamWorkItem -Id 30 -Relations $relations + +``` +Adds work items 55 and 56 as children of 30 + +### Example 5 + +```powershell +$relations = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Duplicate -Operation Add -Comment "is it dupllicate?" +Update-VSTeamWorkItem -Id 30 -Relations $relations + +``` +Adds work items 55 as duplicate of 30 + +### Example 6 + +```powershell +$relations = Get-VSTeamWiql -Id "f87b028b-0528-47d6-b517-2d82af680295" | + Select-Object -ExpandProperty WorkItems | + New-VSTeamWorkItemRelation -RelationType Related +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Adds all work items returned by a query as related to work item 30 + +## PARAMETERS + +### ImputObject + +Operation: Intended for fluent syntax (see Example 2) + +```yaml +Type: PSCustomObject[] +Parameter Sets: ByObject +Required: True +Accept pipeline input: true +``` + +### Id + +Related WorkItem id + +This is not the work item to be updated, but the work item that will be part of the relations of the updated work item +Can be used as parameter or pass a list of ID's or a list of work items from the pipeline + +```yaml +Type: int[] +Parameter Sets: ByID,ByObject +Required: True +Accept pipeline input: true +``` + +### RelationType + +Intended for fluent pipeline (see Example 2) + +```yaml +Type: String[] +Parameter Sets: ByRelation +Required: True +Accept pipeline input: true (ByPropertyName) +``` + +### RelationType + +Relation type name + +You can tab complete from a list of available relation types. Also you can get a list of relation types using the Get-VSTeamWorkItemRelationType CmdLet + +```yaml +Type: string +Required: True +``` + +### Operation + +Add a relation or Remove a relation or Replace a relation + +```yaml +Type: string +Required: False +Default value: Add +Accepted values: Add, Remove, Replace +``` + +## OUTPUTS + +### vsteam_lib.WorkItemRelation + +## NOTES + +This CmdLet do not modify any work item, just generates a JSON Patch compatible object that describes the updates to be applied using the Update-VSTeamWorkItem + + + +## RELATED LINKS + +- [Get-VSTeamWorkItemRelationType](Get-VSTeamWorkItemRelationType.md) +- [Get-VSTeamWorkItem](Get-VSTeamWorkItem.md) +- [Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) \ No newline at end of file diff --git a/.docs/synopsis/New-VSTeamWorkItemRelation.md b/.docs/synopsis/New-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..e73363816 --- /dev/null +++ b/.docs/synopsis/New-VSTeamWorkItemRelation.md @@ -0,0 +1 @@ +Helper cmdlet that creates an in-memory Relation object to facilitate building the -Relations parameter in Update-VSTeamWorkItem \ No newline at end of file diff --git a/Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs b/Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs new file mode 100644 index 000000000..59f30346d --- /dev/null +++ b/Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs @@ -0,0 +1,19 @@ +using System.Management.Automation; + +namespace vsteam_lib +{ + public sealed class RelationTypeToReferenceNameAttribute : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + var value = inputData?.ToString(); + + if (!string.IsNullOrEmpty(value)) + { + value = RelationTypeCache.GetReferenceName(value); + } + + return value; + } + } +} diff --git a/Source/Classes/Cache/RelationTypeCache.cs b/Source/Classes/Cache/RelationTypeCache.cs new file mode 100644 index 000000000..f0e8bd403 --- /dev/null +++ b/Source/Classes/Cache/RelationTypeCache.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; + +namespace vsteam_lib +{ + public static class RelationTypeCache + { + private const string commandName = "Get-VSTeamWorkItemRelationType"; + internal static InternalCache Cache { get; } = new InternalCache(commandName, "Name", false); + + internal static bool HasCacheExpired => Cache.HasCacheExpired; + internal static Dictionary ReferenceNames { get; } = new Dictionary(); + + public static void Invalidate() => Cache.Invalidate(); + + public static void Update(Dictionary cacheItems) + { + ReferenceNames.Clear(); + IEnumerable list; + + // If a list is passed in use it. If not call Get-VSTeamWorkItemRelationType + if (null == cacheItems) + { + Cache.Shell.Commands.Clear(); + + // Use this to store the relationNames + var relations = Cache.Shell.AddCommand(commandName) + .AddParameter("Usage", "WorkItemLink") + .Invoke(); + + foreach (var relation in relations) + { + ReferenceNames.Add(relation.Properties["Name"].Value.ToString(), relation.Properties["ReferenceName"].Value.ToString()); + } + + // This will return just the names + list = Cache.Shell.AddCommand("Select-Object") + .AddParameter("ExpandProperty", "Name") + .AddCommand("Sort-Object") + .Invoke(relations); + + } + else + { + foreach(var relation in cacheItems) { + ReferenceNames.Add(relation.Key, relation.Value); + } + list = cacheItems.Keys; + } + + Cache.PreFill(list); + } + + public static IEnumerable GetCurrent() + { + if (HasCacheExpired) + { + Update(null); + } + + return Cache.Values; + } + + public static string GetReferenceName(string name) + { + return ReferenceNames[name]; + } + } +} diff --git a/Source/Classes/Completer/WorkItemRelationTypeCompleter.cs b/Source/Classes/Completer/WorkItemRelationTypeCompleter.cs new file mode 100644 index 000000000..856ff520c --- /dev/null +++ b/Source/Classes/Completer/WorkItemRelationTypeCompleter.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Management.Automation; +using System.Management.Automation.Abstractions; +using System.Management.Automation.Language; + +namespace vsteam_lib +{ + public class WorkItemRelationTypeCompleter : BaseCompleter + { + /// + /// This constructor is used when running in a PowerShell session. It cannot be + /// loaded in a unit test. + /// + [ExcludeFromCodeCoverage] + public WorkItemRelationTypeCompleter() : base() { } + + /// + /// This constructor is used during unit testings + /// + /// fake instance of IPowerShell used for testing + public WorkItemRelationTypeCompleter(IPowerShell powerShell) : base(powerShell) { } + + public override IEnumerable CompleteArgument(string commandName, + string parameterName, + string wordToComplete, + CommandAst commandAst, + IDictionary fakeBoundParameters) + { + var values = new List(); + + SelectValues(wordToComplete, RelationTypeCache.GetCurrent(), values); + return values; + } + } +} \ No newline at end of file diff --git a/Source/Private/applyTypes.ps1 b/Source/Private/applyTypes.ps1 index e39474148..551bcd7b9 100644 --- a/Source/Private/applyTypes.ps1 +++ b/Source/Private/applyTypes.ps1 @@ -194,4 +194,11 @@ function _applyTypesToWorkItemRelationType { param ($item) $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.WorkItemRelationType') +} + +function _applyTypesToWorkItemRelation { + [CmdletBinding()] + param ($item) + + $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.WorkItemRelation') } \ No newline at end of file diff --git a/Source/Public/New-VSTeamWorkItemRelation.ps1 b/Source/Public/New-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..f8471ebf3 --- /dev/null +++ b/Source/Public/New-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,48 @@ +function New-VSTeamWorkItemRelation { + [CmdletBinding(DefaultParameterSetName="ById", HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/New-VSTeamWorkItemRelation')] + param( + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="ByObject")] + [PSCustomObject[]]$ImputObject, + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="ById")] + [Parameter(Mandatory, ParameterSetName="ByObject")] + [int[]]$Id, + [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] + [vsteam_lib.RelationTypeToReferenceNameAttribute()] + [Parameter(Mandatory)] + [string]$RelationType, + [ValidateSet('Add', 'Remove', 'Replace')] + [string]$Operation = 'Add', + [string]$Comment + ) + + process { + if ($PSCmdlet.ParameterSetName -eq "ByObject") { + $ImputObject + } else { + foreach ($item in $Id) { + $result = [PSCustomObject]@{ + Id = $item + RelationType = $RelationType + Operation = $Operation.ToLower() + Comment = $Comment + } + _applyTypesToWorkItemRelation $result + $result + } + } + } + + end { + if ($PSCmdlet.ParameterSetName -eq "ByObject") { + $result = [PSCustomObject]@{ + Id = $Id[0] + RelationType = $RelationType + Operation = $Operation.ToLower() + Comment = $Comment + } + _applyTypesToWorkItemRelation $result + $result + } + } + +} \ No newline at end of file diff --git a/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml b/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml new file mode 100644 index 000000000..d825b4702 --- /dev/null +++ b/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml @@ -0,0 +1,45 @@ + + + + + vsteam_lib.WorkItemRelation.TableView + + vsteam_lib.WorkItemRelation + + + + + + + + + + + + + + + + + + + + + Id + + + RelationType + + + Operation + + + Comment + + + + + + + + \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json b/Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json new file mode 100644 index 000000000..6c719cbb9 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json @@ -0,0 +1,110 @@ +[ + { + "name": "Affects", + "referenceName": "Microsoft.VSTS.Common.Affects-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Affected By", + "referenceName": "Microsoft.VSTS.Common.Affects-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Produces For", + "referenceName": "System.LinkTypes.Remote.Dependency-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Consumes From", + "referenceName": "System.LinkTypes.Remote.Dependency-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Referenced By", + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "References", + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Tested By", + "referenceName": "Microsoft.VSTS.Common.TestedBy-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Tests", + "referenceName": "Microsoft.VSTS.Common.TestedBy-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Test Case", + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Shared Steps", + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Duplicate", + "referenceName": "System.LinkTypes.Duplicate-Forward", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Duplicate Of", + "referenceName": "System.LinkTypes.Duplicate-Reverse", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Successor", + "referenceName": "System.LinkTypes.Dependency-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Predecessor", + "referenceName": "System.LinkTypes.Dependency-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Child", + "referenceName": "System.LinkTypes.Hierarchy-Forward", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Parent", + "referenceName": "System.LinkTypes.Hierarchy-Reverse", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Related", + "referenceName": "System.LinkTypes.Related", + "Usage": "workItemLink", + "Topology": "network" + }, + { + "name": "Remote Related", + "referenceName": "System.LinkTypes.Remote.Related", + "Usage": "workItemLink", + "Topology": "network" + } + ] \ No newline at end of file diff --git a/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..6c380b8d8 --- /dev/null +++ b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,80 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelation' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + Mock Get-VSTeamWorkItem { + return [PSCustomObject]@{ + Id = 55 + Title = "my title" + } + } + + $relations = Open-SampleFile 'Get-VSTeamWorkItemRelationTypePSObject.json' + $relationsDict = [System.Collections.Generic.Dictionary[string, string]]::new() + foreach ($r in $relations) { + $relationsDict.Add($r.Name, $r.ReferenceName) + } + [vsteam_lib.RelationTypeCache]::Update($relationsDict); + + } + + Context 'New-VSTeamWorkItemRelation' { + It 'Sould return a Relation object' { + $expected = [PSCustomObject]@{ + Id = 55 + Operation = "add" + RelationType = "System.LinkTypes.Hierarchy-Reverse" + Comment = "MyComment" + } + + $actual = New-VSTeamWorkItemRelation -RelationType Parent -Id 55 -Comment "MyComment" + + $actual.Id | Should -Be $expected.Id + $actual.Operation | Should -Be $expected.Operation + $actual.RelationType | Should -Be $expected.RelationType + $actual.Comment | Should -Be $expected.Comment + } + + + It 'With list pipeline should return an array of relations' { + $actual = 55, 66, 77 | New-VSTeamWorkItemRelation -RelationType Child + + $actual | Should -HaveCount 3 + $actual[0].Id | Should -Be "55" + $actual[1].Id | Should -Be "66" + $actual[2].Id | Should -Be "77" + $actual[0].RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + } + + It 'With chained calls should return an array of relations' { + $actual = New-VSTeamWorkItemRelation -RelationType Child -Id 55 | + New-VSTeamWorkItemRelation -RelationType Child -Id 66 | + New-VSTeamWorkItemRelation -RelationType Child -Id 77 + + $actual | Should -HaveCount 3 + $actual[0].Id | Should -Be "55" + $actual[1].Id | Should -Be "66" + $actual[2].Id | Should -Be "77" + $actual[0].RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + } + + It 'With WorkItem in the pipeline should use the workItem.Id' { + $actual = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Child + $actual.Id | Should -Be "55" + $actual.Operation | Should -Be "add" + $actual.RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + } + + It 'Objects have vsteam_lib.WorkItemRelation TypeName' { + $actual = New-VSTeamWorkItemRelation -RelationType Child -Id 55 | + New-VSTeamWorkItemRelation -RelationType Child -Id 66 + $actual[0].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" + $actual[1].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" + } + } +} \ No newline at end of file diff --git a/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs b/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs new file mode 100644 index 000000000..462e400b0 --- /dev/null +++ b/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs @@ -0,0 +1,37 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace vsteam_lib.Test +{ + [TestClass] + [ExcludeFromCodeCoverage] + public class RelationTypeToReferenceNameAttributeTest + { + [TestMethod] + public void RelationTypeTransformToReferenceNameAttribute_Null() + { + // Arrange + string expected = null; + var target = new RelationTypeToReferenceNameAttribute(); + + // Act + var actual = target.Transform(null, null); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void RelationTypeTransformToReferenceNameAttribute_Name_Not_Found() + { + // Arrange + var target = new RelationTypeToReferenceNameAttribute(); + + // Act + var actual = target.Transform(null, "NonExistingName"); + } + + } +} diff --git a/Tests/library/Cache/RelationTypeCacheTests.cs b/Tests/library/Cache/RelationTypeCacheTests.cs new file mode 100644 index 000000000..cb0007420 --- /dev/null +++ b/Tests/library/Cache/RelationTypeCacheTests.cs @@ -0,0 +1,145 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Management.Automation; + +namespace vsteam_lib.Test +{ + [TestClass] + [ExcludeFromCodeCoverage] + public class RelationTypeCacheTests + { + private readonly Collection _empty = new Collection(); + private readonly Collection _emptyStrings = new Collection(); + private readonly Collection _relationNames = new Collection() { "Produces For", "Predecessor", "Parent" }; + private readonly Collection _relations = new Collection() { + PSObject.AsPSObject(new {Name = "Produces For", ReferenceName = "System.LinkTypes.Remote.Dependency-Forward" }), + PSObject.AsPSObject(new {Name = "Predecessor", ReferenceName = "System.LinkTypes.Dependency-Reverse" }), + PSObject.AsPSObject(new {Name = "Parent", ReferenceName = "System.LinkTypes.Hierarchy-Reverse" }) + }; + + [TestMethod] + public void RelationTypeCache_HasCacheExpired() + { + // Arrange + var expected = true; + + // Act + RelationTypeCache.Invalidate(); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.HasCacheExpired, "Cache should be expired"); + + // Act + RelationTypeCache.Update(new Dictionary()); + + // Assert + Assert.AreNotEqual(expected, RelationTypeCache.HasCacheExpired, "Cache should not be expired"); + } + + [TestMethod] + public void RelationTypeCache_Update_With_Null_List() + { + // Arrange + int expected = 3; + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke(this._relations).Returns(this._relationNames); + RelationTypeCache.Cache.Shell = ps; + + // Act + RelationTypeCache.Update(null); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.Cache.Values.Count); + } + + [TestMethod] + public void RelationTypeCache_GetCurrent() + { + // Arrange + var expected = 3; + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke(this._relations).Returns(this._relationNames); + + ps.Invoke().Returns(this._relationNames); + RelationTypeCache.Invalidate(); + RelationTypeCache.Cache.Shell = ps; + + // Act + var actual = RelationTypeCache.GetCurrent(); + + // Assert + Assert.AreEqual(expected, actual.Count()); + } + + [TestMethod] + public void RelationTypeCache_Update_Returns_Null() + { + // Arrange + var expected = 0; + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._empty); + ps.Invoke().Returns(_emptyStrings); + RelationTypeCache.Cache.Shell = ps; + + // Act + RelationTypeCache.Update(null); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.Cache.Values.Count); + } + + [TestMethod] + public void RelationTypeCache_Update_With_Empty_List() + { + // Arrange + var expected = 0; + var ps = BaseTests.PrepPowerShell(); + RelationTypeCache.Cache.Shell = ps; + + // Act + RelationTypeCache.Update(new Dictionary()); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.Cache.Values.Count); + } + + [TestMethod] + public void RelationTypeCache_Get_ReferenceName() + { + // Arrange + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke().Returns(this._relationNames); + RelationTypeCache.Cache.Shell = ps; + + var expected = "System.LinkTypes.Hierarchy-Reverse"; + + // Force an update + RelationTypeCache.Update(null); + + // Act + var actual = RelationTypeCache.GetReferenceName("Parent"); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void RelationTypeCache_Get_ReferenceName_Throws_When_Not_Found() + { + // Arrange + RelationTypeCache.Update(new Dictionary()); + + // Act + var actual = RelationTypeCache.GetReferenceName("NonExistingName"); + + } + } +} diff --git a/Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs b/Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs new file mode 100644 index 000000000..218682449 --- /dev/null +++ b/Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs @@ -0,0 +1,60 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Management.Automation; + +namespace vsteam_lib.Test +{ + [TestClass] + [ExcludeFromCodeCoverage] + public class WorkItemRelationTypeCompleterTest + { + + private readonly Collection _values = new Collection() { "Produces For", "Predecessor", "Parent" }; + + private readonly Collection _relationObjects = new Collection() { + PSObject.AsPSObject(new {Name = "Produces For", ReferenceName = "System.LinkTypes.Remote.Dependency-Forward" }), + PSObject.AsPSObject(new {Name = "Predecessor", ReferenceName = "System.LinkTypes.Dependency-Reverse" }), + PSObject.AsPSObject(new {Name = "Parent", ReferenceName = "System.LinkTypes.Hierarchy-Reverse" }) + }; + + + private readonly Dictionary _relations = new Dictionary { + + { "'Produces For'", "System.LinkTypes.Remote.Dependency-Forward" }, + { "Predecessor", "System.LinkTypes.Dependency-Reverse" }, + { "Parent", "System.LinkTypes.Hierarchy-Reverse" } + }; + + [TestMethod] + public void WorkImteRelations_Exercise() + { + // Arrange + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relationObjects); + ps.Invoke(this._relationObjects).Returns(this._values); + RelationTypeCache.Cache.Shell = ps; + RelationTypeCache.Invalidate(); + + var target = new WorkItemRelationTypeCompleter(ps); + var fakeBoundParameters = new Dictionary(); + + // Act + var actual = target.CompleteArgument(string.Empty, string.Empty, "P", null, fakeBoundParameters); + + // Assert + Assert.AreEqual(_relations.Count, actual.Count()); + var e = actual.GetEnumerator(); + foreach (var relation in _relations) + { + e.MoveNext(); + Assert.AreEqual(relation.Key, e.Current.CompletionText); + } + + + } + } +} diff --git a/config.json b/config.json index 6b14dc454..7511f790f 100644 --- a/config.json +++ b/config.json @@ -129,7 +129,8 @@ "vsteam_lib.ResourceArea.TableView.ps1xml", "vsteam_lib.PullRequest.TableView.ps1xml", "vsteam_lib.Policy.TableView.ps1xml", - "vsteam_lib.WorkItemRelationType.TableView.ps1xml" + "vsteam_lib.WorkItemRelationType.TableView.ps1xml", + "vsteam_lib.WorkItemRelation.TableView.ps1xml" ] } } \ No newline at end of file From 4159e29a8b63d2e78b2f62efcddba5a2749aad52 Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Sat, 17 Jun 2023 21:32:02 +0200 Subject: [PATCH 03/19] fix PSScriptAnalyzer warning --- Source/Public/New-VSTeamWorkItemRelation.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Public/New-VSTeamWorkItemRelation.ps1 b/Source/Public/New-VSTeamWorkItemRelation.ps1 index f8471ebf3..e8ad41a70 100644 --- a/Source/Public/New-VSTeamWorkItemRelation.ps1 +++ b/Source/Public/New-VSTeamWorkItemRelation.ps1 @@ -1,4 +1,5 @@ function New-VSTeamWorkItemRelation { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope="Function", Justification='We still are not doing any persistent changes')] [CmdletBinding(DefaultParameterSetName="ById", HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/New-VSTeamWorkItemRelation')] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="ByObject")] From db50d1ca21f88b5d46a00e0c29233657d225d08b Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Sun, 18 Jun 2023 00:15:13 +0200 Subject: [PATCH 04/19] fix help file for New-VSTeamWorkItemRelation --- .docs/New-VSTeamWorkItemRelation.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.docs/New-VSTeamWorkItemRelation.md b/.docs/New-VSTeamWorkItemRelation.md index 552584ff0..713737d73 100644 --- a/.docs/New-VSTeamWorkItemRelation.md +++ b/.docs/New-VSTeamWorkItemRelation.md @@ -107,10 +107,9 @@ Accept pipeline input: true Intended for fluent pipeline (see Example 2) ```yaml -Type: String[] +Type: String Parameter Sets: ByRelation Required: True -Accept pipeline input: true (ByPropertyName) ``` ### RelationType @@ -135,6 +134,15 @@ Default value: Add Accepted values: Add, Remove, Replace ``` +### Comment + +Add a comment to the relation + +```yaml +Type: string +Required: False +``` + ## OUTPUTS ### vsteam_lib.WorkItemRelation @@ -147,6 +155,6 @@ This CmdLet do not modify any work item, just generates a JSON Patch compatible ## RELATED LINKS -- [Get-VSTeamWorkItemRelationType](Get-VSTeamWorkItemRelationType.md) -- [Get-VSTeamWorkItem](Get-VSTeamWorkItem.md) -- [Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) \ No newline at end of file +[Get-VSTeamWorkItemRelationType](Get-VSTeamWorkItemRelationType.md) +[Get-VSTeamWorkItem](Get-VSTeamWorkItem.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) \ No newline at end of file From 5981da1a8196ca36331249fd28b8ecdac087b8cb Mon Sep 17 00:00:00 2001 From: mnieto Date: Mon, 3 Jul 2023 00:48:40 +0200 Subject: [PATCH 05/19] function New-VSTeamWorkItemRelation --- .docs/New-VSTeamWorkItemRelation.md | 76 +++++++++++-------- Source/Public/New-VSTeamWorkItemRelation.ps1 | 50 +++++++++--- Source/Public/Update-VSTeamWorkItem.ps1 | 35 +++++++++ ...team_lib.WorkItemRelation.TableView.ps1xml | 6 ++ .../New-VSTeamWorkItemRelation.Tests.ps1 | 6 ++ 5 files changed, 131 insertions(+), 42 deletions(-) diff --git a/.docs/New-VSTeamWorkItemRelation.md b/.docs/New-VSTeamWorkItemRelation.md index 713737d73..72d1cfc68 100644 --- a/.docs/New-VSTeamWorkItemRelation.md +++ b/.docs/New-VSTeamWorkItemRelation.md @@ -17,53 +17,55 @@ ### Example 1 ```powershell -New-VSTeamWorkItemRelation -RelationType Duplicate -Id 55 -Operation Remove -Comment "not needed" +New-VSTeamWorkItemRelation -RelationType Duplicate -Id 55 -Comment "My comment" -Id RelationType Operation Comment --- ------------ --------- ------- -55 System.LinkTypes.Duplicate-Forward remove not needed +ID RelationType Operation Index Comment +-- ------------ --------- ----- ------- +55 System.LinkTypes.Hierarchy-Reverse add - My comment ``` - Simple invocation, returns a Relation object. ### Example 2 ```powershell -$relations = New-VSTeamWorkItemRelation -RelationType Related -Operation Remove -Id 55 | - New-VSTeamWorkItemRelation -RelationType Related -Operation Remove -Id 66 | - New-VSTeamWorkItemRelation -RelationType Related -Operation Remove -Id 77 | +$relations = New-VSTeamWorkItemRelation -Operation Remove -Index 0 | + New-VSTeamWorkItemRelation -Operation Remove -Index 1 Update-VSTeamWorkItem -Id 30 -Relations $relations ``` -Removes work items 55, 66 and 77 relations from work item 30 +Removes work first 2 links from work item 30 ### Example 3 ```powershell $relations =@() -$relations += New-VSTeamWorkItemRelation -RelationType Related -Id 55 -Operation Remove -$relations += New-VSTeamWorkItemRelation -RelationType Related -Id 66 -Operation Add +$relations += New-VSTeamWorkItemRelation -Operation Remove -Index 0 +$relations += New-VSTeamWorkItemRelation -RelationType Related -Id 66 Update-VSTeamWorkItem -Id 30 -Relations $relations ``` Similar to example 2, but managing the relations collection directly +Pay attention that the first relation, for Operation Remove, only the Index parameter is provided. +In the second relation, when provided the Id, it's necessary to provide the RealationType. The operation will be always Add. ### Example 4 ```powershell -$relations = 55,66 | New-VSTeamWorkItemRelation -RelationType Child -Operation Add +$relations = 55,66 | New-VSTeamWorkItemRelation -RelationType Child Update-VSTeamWorkItem -Id 30 -Relations $relations ``` Adds work items 55 and 56 as children of 30 +Pay attention that this use case, passing a list of IDs from pipeline, has sense only for 'Add' operation, and because this is the defaut operation value, we can ommit the Operation parameter ### Example 5 ```powershell -$relations = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Duplicate -Operation Add -Comment "is it dupllicate?" +$relations = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Duplicate -Comment "is it dupllicate?" Update-VSTeamWorkItem -Id 30 -Relations $relations ``` Adds work items 55 as duplicate of 30 +Pay attention that this use case, passing a list of work items from pipeline, has sense only for 'Add' operation, and because this is the defaut operation value and the Operation parameter is not allowed when you provide the Id ### Example 6 @@ -74,6 +76,26 @@ $relations = Get-VSTeamWiql -Id "f87b028b-0528-47d6-b517-2d82af680295" | Update-VSTeamWorkItem -Id 30 -Relations $relations ``` Adds all work items returned by a query as related to work item 30 +Pay attention that this use case, passing a list of work items from pipeline, has sense only for 'Add' operation, and because this is the defaut operation value, we can ommit the Operation parameter + +### Example 7 +```powershell +$relation = New-VSTeamWorkItemRelation -RelationType Related -Operation Replace -Comment "updated comment" +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Updates the comment of a relation. The replace operation only supports comment update. +If you really need to change a relation, like re-parent a work item, you need to create two relations: first, remove and then add operations. + +### Example 8 +```powershell +$relations =@() +$id = Get-VSTeamWorkItem -id 30 -Expand Relation +for ($i=0; $i -lt $id.relations.Count; $i++) { + $relations += New-VSTeamWorkItemRelation -Operation Remove -Index $i +} +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Removes all the links from work item 30 ## PARAMETERS @@ -104,39 +126,31 @@ Accept pipeline input: true ### RelationType -Intended for fluent pipeline (see Example 2) - -```yaml -Type: String -Parameter Sets: ByRelation -Required: True -``` - -### RelationType - -Relation type name - +Specify the relation type. The relation name is translated to the technical name. You can tab complete from a list of available relation types. Also you can get a list of relation types using the Get-VSTeamWorkItemRelationType CmdLet +Not allowed when you specify an index in the remove or replace operations ```yaml -Type: string -Required: True +Type: String +Parameter Sets: ByID,ByRelation +Required: True (in ByID parameterset) ``` ### Operation -Add a relation or Remove a relation or Replace a relation +Remove or Replace a relation +The Add operation is implicit when the Id parameter is used. So this parameter is only valid when Index is specified ```yaml Type: string +Parameter Sets: ByIndex,ByRelation Required: False -Default value: Add -Accepted values: Add, Remove, Replace +Accepted values: Remove, Replace ``` ### Comment -Add a comment to the relation +Add (or edit -with Replace operation-) a comment to the relation ```yaml Type: string diff --git a/Source/Public/New-VSTeamWorkItemRelation.ps1 b/Source/Public/New-VSTeamWorkItemRelation.ps1 index e8ad41a70..5a7fa852e 100644 --- a/Source/Public/New-VSTeamWorkItemRelation.ps1 +++ b/Source/Public/New-VSTeamWorkItemRelation.ps1 @@ -5,41 +5,69 @@ function New-VSTeamWorkItemRelation { [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="ByObject")] [PSCustomObject[]]$ImputObject, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="ById")] - [Parameter(Mandatory, ParameterSetName="ByObject")] + [Parameter(ParameterSetName="ByObject")] [int[]]$Id, [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] [vsteam_lib.RelationTypeToReferenceNameAttribute()] - [Parameter(Mandatory)] + [Parameter(Mandatory, ParameterSetName="ById")] + [Parameter(ParameterSetName="ByObject")] [string]$RelationType, - [ValidateSet('Add', 'Remove', 'Replace')] - [string]$Operation = 'Add', + [ValidateSet('Remove', 'Replace')] + [Parameter(Mandatory, ParameterSetName="ByIndex")] + [Parameter(ParameterSetName="ByObject")] + [string]$Operation, + [Parameter(Mandatory, ParameterSetName="ByIndex")] + [Parameter(ParameterSetName="ByObject")] + [int]$Index, [string]$Comment ) process { if ($PSCmdlet.ParameterSetName -eq "ByObject") { $ImputObject - } else { + } elseif ($PSCmdlet.ParameterSetName -eq "ById") { foreach ($item in $Id) { $result = [PSCustomObject]@{ Id = $item RelationType = $RelationType - Operation = $Operation.ToLower() + Operation = 'add' Comment = $Comment + Index = '-' } _applyTypesToWorkItemRelation $result $result } + } else { + $result = [PSCustomObject]@{ + Id = $null + RelationType = $RelationType + Operation = $Operation.ToLower() + Comment = $Comment + Index = $index + } + _applyTypesToWorkItemRelation $result + $result } } end { if ($PSCmdlet.ParameterSetName -eq "ByObject") { - $result = [PSCustomObject]@{ - Id = $Id[0] - RelationType = $RelationType - Operation = $Operation.ToLower() - Comment = $Comment + if ($null -eq $PSBoundParameters.Index) { + $result = [PSCustomObject]@{ + Id = $Id[0] + RelationType = $RelationType + Operation = "add" + Comment = $Comment + Index = "-" + } + } else { + $result = [PSCustomObject]@{ + Id = $null + RelationType = $null + Operation = $Operation.ToLower() + Comment = $Comment + Index = $PSBoundParameters.Index + } } _applyTypesToWorkItemRelation $result $result diff --git a/Source/Public/Update-VSTeamWorkItem.ps1 b/Source/Public/Update-VSTeamWorkItem.ps1 index b5d642bb5..b9ecb9a03 100644 --- a/Source/Public/Update-VSTeamWorkItem.ps1 +++ b/Source/Public/Update-VSTeamWorkItem.ps1 @@ -20,6 +20,9 @@ function Update-VSTeamWorkItem { [Parameter(Mandatory = $false)] [hashtable]$AdditionalFields, + [Parameter(Mandatory = $false)] + [PSCustomObject[]]$Relations, + [switch] $Force ) @@ -68,6 +71,38 @@ function Update-VSTeamWorkItem { } } + foreach ($relation in $Relations) { + switch ($relation.Operation) { + "add" { + $body += @{ + op = $relation.Operation + path = "/relations/-" + value = @{ + rel = $relation.RelationType + url = _buildRequestURI -area "wit" -resource "workItems" -id $relation.Id + attributes = @{ + comment = $relation.Comment + } + } + } + } + "remove" { + $body += @{ + op = $relation.Operation + path = "/relations/$($relation.Index)" + } + } + "replace" { + $body += @{ + op = $relation.Operation + path = "/relations/$($relation.Index)/attributes/comment" + value = $relation.Comment + } + } + } + } + + # It is very important that even if the user only provides # a single value above that the item is an array and not # a single object or the call will fail. diff --git a/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml b/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml index d825b4702..a64615b3c 100644 --- a/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml +++ b/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml @@ -17,6 +17,9 @@ + + + @@ -33,6 +36,9 @@ Operation + + Index + Comment diff --git a/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 index 6c380b8d8..b813bbf18 100644 --- a/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 +++ b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 @@ -76,5 +76,11 @@ Describe 'VSTeamWorkItemRelation' { $actual[0].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" $actual[1].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" } + + It 'With Index paramter ID nor RelationType are needed' { + $actual = New-VSTeamWorkItemRelation -Index 0 -Operation Remove + $actual.Id | Should -BeNullOrEmpty + $actual.RelationType | Should -BeNullOrEmpty + } } } \ No newline at end of file From e27cfa2fc1dd1b6cc96c8dec65c71d90a19dab98 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 7 Jul 2023 21:31:28 +0200 Subject: [PATCH 06/19] document and test Update-VSTeamWorkItem with relations --- .docs/Update-VSTeamWorkItem.md | 10 ++++ .../Get-VSTeamWorkItem-Id16-After.json | 56 +++++++++++++++++++ .../tests/Update-VSTeamWorkItem.Tests.ps1 | 28 ++++++++++ 3 files changed, 94 insertions(+) create mode 100644 Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json diff --git a/.docs/Update-VSTeamWorkItem.md b/.docs/Update-VSTeamWorkItem.md index af75c75d1..f5a4a41c0 100644 --- a/.docs/Update-VSTeamWorkItem.md +++ b/.docs/Update-VSTeamWorkItem.md @@ -106,6 +106,14 @@ Type: Hashtable Required: False ``` +### Relations +Array of vsteam_lib.WorkItemRelation that describes the relations to be added, removed or updated in/from the work item + +```yaml +Type: PSCustomObject[] +Required: False +``` + ## INPUTS @@ -125,3 +133,5 @@ Any of the basic work item parameters defined in this method, will cause an exce ## RELATED LINKS + +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json b/Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json new file mode 100644 index 000000000..c797a3a37 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json @@ -0,0 +1,56 @@ +{ + "id": 16, + "rev": 2, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing", + "System.Parent": 80 + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Reverse", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/80", + "attributes": { + "isLocked": false, + "comment": "Added from AzFunction", + "name": "Parent" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 b/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 index b03866801..4854629f4 100644 --- a/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 +++ b/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 @@ -3,6 +3,7 @@ Set-StrictMode -Version Latest Describe 'VSTeamWorkItem' { BeforeAll { . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" Mock _getInstance { return 'https://dev.azure.com/test' } } @@ -10,6 +11,8 @@ Describe 'VSTeamWorkItem' { Context 'Update-VSTeamWorkItem' { BeforeAll { Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16.json' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16-After.json' } -ParameterFilter { $relations -ne $null } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 80 } } It 'Without Default Project should update work item' { @@ -124,5 +127,30 @@ Describe 'VSTeamWorkItem' { ## Act / Assert { Update-VSTeamWorkItem 1 -Title Test1 -Description Testing -AdditionalFields $additionalFields } | Should -Throw } + + It 'With relations should update relations' { + ## Arrange + $Global:PSDefaultParameterValues["*-vsteam*:projectName"] = 'test' + $relations = New-VSTeamWorkItemRelation -Id 80 -RelationType Parent + + ## Act / Assert + $wi = Update-VSTeamWorkItem 1 -Title Test1 -Description Testing -Relations $relations -Force + $wi.Fields."System.Parent" | Should -Be 80 + Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Patch' -and + $Body -like '`[*' -and # Make sure the body is an array + $Body -like '*Test1*' -and + $Body -like '*Testing*' -and + $Body -like '*/fields/System.Title*' -and + $Body -like '*/fields/System.Description*' -and + $Body -like '*/relations/-*' -and + $Body -like '*`]' -and # Make sure the body is an array + $ContentType -eq 'application/json-patch+json; charset=utf-8' -and + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/1?api-version=$(_getApiVersion Core)" + } + + + + } } } \ No newline at end of file From 80854e5cacbe36b2bff263e7462de103e652f4c4 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 7 Jul 2023 21:32:37 +0200 Subject: [PATCH 07/19] function Switch-VSTeamWorkItemParent --- .docs/Switch-VSTeamWorkItemParent.md | 80 +++++++++++++++++++ .docs/synopsis/Switch-VSTeamWorkItemParent.md | 3 + Source/Public/Switch-VSTeamWorkItemParent.ps1 | 48 +++++++++++ .../Get-VSTeamWorkItem-Id55-After.json | 56 +++++++++++++ .../Get-VSTeamWorkItem-Id55-Before.json | 56 +++++++++++++ .../Switch-VSTeamWorkItemParent.Tests.ps1 | 57 +++++++++++++ 6 files changed, 300 insertions(+) create mode 100644 .docs/Switch-VSTeamWorkItemParent.md create mode 100644 .docs/synopsis/Switch-VSTeamWorkItemParent.md create mode 100644 Source/Public/Switch-VSTeamWorkItemParent.ps1 create mode 100644 Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json create mode 100644 Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json create mode 100644 Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 diff --git a/.docs/Switch-VSTeamWorkItemParent.md b/.docs/Switch-VSTeamWorkItemParent.md new file mode 100644 index 000000000..8c4413cb3 --- /dev/null +++ b/.docs/Switch-VSTeamWorkItemParent.md @@ -0,0 +1,80 @@ + + +# Add-VSTeam + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### Example 1: Replace the paretn in 2 work items + +```powershell +50, 51 | Switch-VSTeamWorkItemParent -ParentId 80 +``` +This command replaces the parent in workitems with ID 50 and 51 and assign work item 80 as parent +If any of the IDs provided don't have a parent, they shall remain they are. + +### Example 2: Replace the paretn in 2 work items + +```powershell +Switch-VSTeamWorkItemParent Id 50,51 -ParentId 80 -AddParent +``` +This command replaces the parent in workitems with ID 50 and 51 and assign work item 80 as parent +If any of the IDs provided don't have a parent, the work item 80 is assigned + +## PARAMETERS + +### Id + +IDs of the elements whose parent is to be replaced + +```yaml +Type: int[] +Required: True +Position: 0 +Accept pipeline input: true (ByPropertyName, ByValue) +``` + +### ParentId + +Id of the new parent work item + +```yaml +Type: int +Required: True +Position: 1 +``` + +### AddParent + +If present, if the work item currently doen't have a parent, it will assign $ParentId as parent +If not present, if the work item currently doen't have a parent, it shall remain as is. + +```yaml +Type: switch +Required: False +Position: 3 +``` + + + +## INPUTS + +## OUTPUTS + +## NOTES + + + +## RELATED LINKS + +[Get-VSTeam](Get-VSTeamUpdateWorkItem.md) +[Remove-VSTeam](New-VSTeamWorkItemRelation.md) diff --git a/.docs/synopsis/Switch-VSTeamWorkItemParent.md b/.docs/synopsis/Switch-VSTeamWorkItemParent.md new file mode 100644 index 000000000..5ad6537b3 --- /dev/null +++ b/.docs/synopsis/Switch-VSTeamWorkItemParent.md @@ -0,0 +1,3 @@ +Replaces the parent of one or more work items +It also allows to add a parent if the work item currently doesn't have one +If current parent Id is equal to the new one, no action is done \ No newline at end of file diff --git a/Source/Public/Switch-VSTeamWorkItemParent.ps1 b/Source/Public/Switch-VSTeamWorkItemParent.ps1 new file mode 100644 index 000000000..2193695b0 --- /dev/null +++ b/Source/Public/Switch-VSTeamWorkItemParent.ps1 @@ -0,0 +1,48 @@ +function Switch-VSTeamWorkItemParent { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium", HelpUri = 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Switch-VSTeamWorkItemParent')] + param ( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [int[]]$Id, + [Parameter(Mandatory)] + [int]$ParentId, + [switch]$AddParent, + [switch]$Force + ) + + process { + foreach($item in $Id) { + $ParentNeeded = $false + $wi = Get-VSTeamWorkItem -Id $item -Expand Relations + if ($null -eq $wi.Relations -and $AddParent) { + $ParentNeeded = $true + } else { + if ($wi.Relations) { + $relations = $wi | Select-Object -ExpandProperty Relations + $index = 0 + $hasParent = $false + foreach($relation in $relations) { + if ($relation.rel -eq 'System.LinkTypes.Hierarchy-Reverse') { + $hasParent = $true + if ($relation.url.split('/')[-1] -ne $ParentId) { + $patch = New-VSTeamWorkItemRelation -Index $index -Operation Remove | New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent + if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem")) { + $wi = Update-VSTeamWorkItem -Id $item -Relations $patch + } + break + } + } + $index++ + } + if (-not $hasParent -and $AddParent) { + $ParentNeeded = $true + } + } + } + + if ($ParentNeeded -and ($Foce -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem"))) { + $wi = Update-VSTeamWorkItem -Id $item -Relations (New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent) + } + return $wi + } + } +} \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json new file mode 100644 index 000000000..a31b6b139 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json @@ -0,0 +1,56 @@ +{ + "id": 55, + "rev": 2, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing", + "System.Parent": 80 + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Reverse", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/80", + "attributes": { + "isLocked": false, + "comment": "Added from AzFunction", + "name": "Parent" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json new file mode 100644 index 000000000..b9a09510e --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json @@ -0,0 +1,56 @@ +{ + "id": 55, + "rev": 1, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing", + "System.Parent": 70 + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Reverse", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/70", + "attributes": { + "isLocked": false, + "comment": "Added from AzFunction", + "name": "Parent" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 b/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 new file mode 100644 index 000000000..a99a6be5b --- /dev/null +++ b/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 @@ -0,0 +1,57 @@ +Set-StrictMode -Version Latest + +Describe "VSTeamWorkItemParent" { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/Update-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '1.0-unitTests' } #-ParameterFilter { $Service -eq 'Core' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/16*' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16-After.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/16*' -and $Method -eq 'Patch' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-Before.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-After.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' -and $Method -eq 'Patch' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Operation = 'remove'; Index = 0 }} -ParameterFilter { $index -eq 0 } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 80 } + Mock New-VSTeamWorkItemRelation { return @( + [PSCustomObject]@{ Operation = 'remove'; Index = 0 } + [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' } + )} -ParameterFilter { $id -eq 80 -and $ImputObject -ne $null} + } + + Context 'Switch-VSTeamWorkItemParent' { + It 'replaces old parent with new one' { + $actual = Switch-VSTeamWorkItemParent -Id 55 -ParentId 80 + + $actual.Fields."System.Parent" | Should -Be 80 + Should -Invoke Invoke-RestMethod -Exactly -Times 2 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 2 -Scope It + } + It 'should not replace the parent if new parent is the same than old parent' { + $actual = Switch-VSTeamWorkItemParent -Id 55 -ParentId 70 + + $actual.Fields."System.Parent" | Should -Be 70 + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 0 -Scope It + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' } + } + + It 'with AddParent it should add the parent if workitem did not have one' { + $actual = Switch-VSTeamWorkItemParent -Id 16 -ParentId 80 -AddParent + + $actual.Fields."System.Parent" | Should -Be 80 + Should -Invoke Invoke-RestMethod -Exactly -Times 2 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 1 -Scope It + } + + It 'without AddParent it should NOT add the parent if workitem did not have one' { + $actual = Switch-VSTeamWorkItemParent -Id 16 -ParentId 80 + + $actual.Fields."System.Parent" | Should -BeNullOrEmpty + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 0 -Scope It + } + + } +} \ No newline at end of file From 582ad852bc4d84a581202e5488dd72684390a74e Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 15 Jul 2023 17:59:06 +0200 Subject: [PATCH 08/19] fix Switch-VSTeamWorkItemParent when multiple Id are passed as parameter --- Source/Public/Switch-VSTeamWorkItemParent.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Public/Switch-VSTeamWorkItemParent.ps1 b/Source/Public/Switch-VSTeamWorkItemParent.ps1 index 2193695b0..d402b09be 100644 --- a/Source/Public/Switch-VSTeamWorkItemParent.ps1 +++ b/Source/Public/Switch-VSTeamWorkItemParent.ps1 @@ -25,7 +25,7 @@ function Switch-VSTeamWorkItemParent { $hasParent = $true if ($relation.url.split('/')[-1] -ne $ParentId) { $patch = New-VSTeamWorkItemRelation -Index $index -Operation Remove | New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent - if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem")) { + if ($Force -or $pscmdlet.ShouldProcess($item, "Update-WorkItem")) { $wi = Update-VSTeamWorkItem -Id $item -Relations $patch } break @@ -42,7 +42,7 @@ function Switch-VSTeamWorkItemParent { if ($ParentNeeded -and ($Foce -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem"))) { $wi = Update-VSTeamWorkItem -Id $item -Relations (New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent) } - return $wi + $wi } } } \ No newline at end of file From 44e07e75e00b5576f2c049042500cc3398e4d43b Mon Sep 17 00:00:00 2001 From: mnieto Date: Mon, 17 Jul 2023 00:57:04 +0200 Subject: [PATCH 09/19] fix Switch-VSTeamWorkItemParent documentation --- .docs/Switch-VSTeamWorkItemParent.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docs/Switch-VSTeamWorkItemParent.md b/.docs/Switch-VSTeamWorkItemParent.md index 8c4413cb3..84fa17acf 100644 --- a/.docs/Switch-VSTeamWorkItemParent.md +++ b/.docs/Switch-VSTeamWorkItemParent.md @@ -76,5 +76,5 @@ Position: 3 ## RELATED LINKS -[Get-VSTeam](Get-VSTeamUpdateWorkItem.md) -[Remove-VSTeam](New-VSTeamWorkItemRelation.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) From 5fa5349a1f403db45b4a51dfa092704a7babe825 Mon Sep 17 00:00:00 2001 From: mnieto Date: Mon, 17 Jul 2023 02:20:37 +0200 Subject: [PATCH 10/19] function Add-VSTeamWorkItemRelation --- .docs/Add-VSTeamWorkItemRelation.md | 87 +++++++++++++++++++ .docs/synopsis/Add-VSTeamWorkItemRelation.md | 1 + Source/Public/Add-VSTeamWorkItemRelation.ps1 | 20 +++++ .../Add-VSTeamWorkItemRelation-Id55.json | 54 ++++++++++++ .../Add-VSTeamWorkItemRelation.Tests.ps1 | 26 ++++++ 5 files changed, 188 insertions(+) create mode 100644 .docs/Add-VSTeamWorkItemRelation.md create mode 100644 .docs/synopsis/Add-VSTeamWorkItemRelation.md create mode 100644 Source/Public/Add-VSTeamWorkItemRelation.ps1 create mode 100644 Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json create mode 100644 Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 diff --git a/.docs/Add-VSTeamWorkItemRelation.md b/.docs/Add-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..8cd5a25be --- /dev/null +++ b/.docs/Add-VSTeamWorkItemRelation.md @@ -0,0 +1,87 @@ + + +# Add-VSTeam + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### Example 1: Adds workitems as children + +```powershell +50, 51 | Add-VSTeamWorkItemRelation -RelationType Child -OfRelatedId 80 +``` +This command adds 50 and 51 as child of 80 + +### Example 2: Adds workitems as children + +```powershell +Add-VSTeamWorkItemRelation Id 50,51 -RelationType Child -OfRelatedId 80 +``` +This command adds 50 and 51 as child of 80 + +## PARAMETERS + +### Id + +IDs of the elements whose relate with RelatedId + +```yaml +Type: int[] +Required: True +Position: 0 +Accept pipeline input: true (ByPropertyName, ByValue) +``` + +### RelationType + +Specify the relation type. The relation name is translated to the technical name. +You can tab complete from a list of available relation types. Also you can get a list of relation types using the Get-VSTeamWorkItemRelationType CmdLet + +```yaml +Type: String +Required: True +Position: 1 +``` + +### OfRelatedId + +Id of the related work item +All the work items identified by the IDs will be related with this work item. For example, you cannot specify a -RelationType Parent because the +cmdlet will try to assing all the IDs as parent of the related work item + +```yaml +Type: int +Required: True +Position: 2 +``` + + + +## INPUTS + +## OUTPUTS + +### System.String + +Returns the work item identified by OfRelatedId + +## NOTES + +This cmdlet is a shorcut for New-VSTeamWorkItemRelation and Update-VSTeamWorkItem. + + + +## RELATED LINKS + +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) +[Switch-VSTeamWorkItemParent](Switch-VSTeamWorkItemParent.md) diff --git a/.docs/synopsis/Add-VSTeamWorkItemRelation.md b/.docs/synopsis/Add-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..43f9e1ca3 --- /dev/null +++ b/.docs/synopsis/Add-VSTeamWorkItemRelation.md @@ -0,0 +1 @@ +Adds a relationship between different workitems \ No newline at end of file diff --git a/Source/Public/Add-VSTeamWorkItemRelation.ps1 b/Source/Public/Add-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..020e7f093 --- /dev/null +++ b/Source/Public/Add-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,20 @@ +function Add-VSTeamWorkItemRelation { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium", HelpUri = 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Add-VSTeamWorkItemRelation')] + param ( + [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [int[]]$Id, + [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] + [Parameter(Mandatory, Position=1)] + [string]$RelationType, + [Parameter(Mandatory, Position=2)] + [int]$OfRelatedId, + [switch]$Force + ) + + process { + $relations = $Id | New-VSTeamWorkItemRelation -RelationType $RelationType + if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem")) { + Update-VSTeamWorkItem -Id $OfRelatedId -Relations $relations + } + } +} \ No newline at end of file diff --git a/Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json b/Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json new file mode 100644 index 000000000..beee75ea7 --- /dev/null +++ b/Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json @@ -0,0 +1,54 @@ +{ + "id": 55, + "rev": 2, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing" + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Forward", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/70", + "attributes": { + "isLocked": false, + "name": "Child" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..08569720d --- /dev/null +++ b/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,26 @@ +Set-StrictMode -Version Latest + +Describe "VSTeamWorkItemRelation" { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Update-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '1.0-unitTests' } #-ParameterFilter { $Service -eq 'Core' } + Mock Invoke-RestMethod { Open-SampleFile 'Add-VSTeamWorkItemRelation-Id55.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' -and $Method -eq 'Patch' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 70; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 70 } + } + + Context 'Add-VSTeamWorkItemRelation' { + It 'add work items as related of another work item and returns the related work item' { + $actual = Add-VSTeamWorkItemRelation -Id 70 -RelationType Child -OfRelatedId 55 + + $actual.Relations | Should -HaveCount 1 + $actual.Relations[0].url.split('/')[-1] | Should -Be 70 + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 1 -Scope It + } + + } +} \ No newline at end of file From 34c605c0444dc88113cd07766bea8da5765b1c85 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sun, 6 Aug 2023 22:22:40 +0200 Subject: [PATCH 11/19] fix empty cache when not used autocomplete functionality for RelationType parameter --- Source/Classes/Cache/RelationTypeCache.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Classes/Cache/RelationTypeCache.cs b/Source/Classes/Cache/RelationTypeCache.cs index f0e8bd403..6f0d47ddf 100644 --- a/Source/Classes/Cache/RelationTypeCache.cs +++ b/Source/Classes/Cache/RelationTypeCache.cs @@ -63,6 +63,9 @@ public static IEnumerable GetCurrent() public static string GetReferenceName(string name) { + if (ReferenceNames.Keys.Count == 0) { + Update(null); + } return ReferenceNames[name]; } } From 0fcb03e038b3f4bf12a968c169437c6b8f6a5dd8 Mon Sep 17 00:00:00 2001 From: mnieto Date: Thu, 24 Aug 2023 20:10:54 +0200 Subject: [PATCH 12/19] Get-VSTeamWorkItemRelation --- .docs/Get-VSTeamWorkItemRelation.md | 51 +++++++++++++++++++ .docs/synopsis/Get-VSTeamWorkItemRelation.md | 2 + Source/Public/Get-VSTeamWorkItemRelation.ps1 | 16 ++++++ .../Get-VSTeamWorkItemRelation.Tests.ps1 | 29 +++++++++++ 4 files changed, 98 insertions(+) create mode 100644 .docs/Get-VSTeamWorkItemRelation.md create mode 100644 .docs/synopsis/Get-VSTeamWorkItemRelation.md create mode 100644 Source/Public/Get-VSTeamWorkItemRelation.ps1 create mode 100644 Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 diff --git a/.docs/Get-VSTeamWorkItemRelation.md b/.docs/Get-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..95ff07235 --- /dev/null +++ b/.docs/Get-VSTeamWorkItemRelation.md @@ -0,0 +1,51 @@ + + +# Get-VSTeamWorkItemRelation + +## SYNOPSIS + + + +## SYNTAX + +## Description + + + +## EXAMPLES + +### Example 1 + +```powershell +Get-VSTeamWorkItemRelation -Id 55 +``` + +This command retrieves a list of relations from a single work item. + +## PARAMETERS + +### Id +Work item ID + +```yaml +Type: int +``` + + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + + + + +## RELATED LINKS + +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) +[Switch-VSTeamWorkItemParent](Switch-VSTeamWorkItemParent.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) +[Remove-VSTeamWorkItemRelation](Remove-VSTeamWorkItemRelation.md) \ No newline at end of file diff --git a/.docs/synopsis/Get-VSTeamWorkItemRelation.md b/.docs/synopsis/Get-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..f0ff64d70 --- /dev/null +++ b/.docs/synopsis/Get-VSTeamWorkItemRelation.md @@ -0,0 +1,2 @@ +Retrieves a list of relations from a single work item. +This cmdlet is a shortcut for `Get-VSTeamWorkItem -Id $Id -Expand Relations | Select-Object -ExpandProperty Relations` and returns a list of relations compatible with the *-VSTeamWorkItemRelation and Switch-VSTeamWorkItemParent cmdlets diff --git a/Source/Public/Get-VSTeamWorkItemRelation.ps1 b/Source/Public/Get-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..9b30cb26b --- /dev/null +++ b/Source/Public/Get-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,16 @@ +function Get-VSTeamWorkItemRelation { + [CmdletBinding(HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Get-VSTeamWorkItemRelation')] + param ( + [int]$Id + ) + + $relations = Get-VSTeamWorkItem -Id $Id -Expand Relations -Verbose | Select-Object -ExpandProperty Relations + $i = 0 + foreach ($relation in $relations) { + $relatedId = $relation.Url.split("/")[-1] + $r = New-VSTeamWorkItemRelation -Id $relatedId -RelationType $relation.Attributes.Name -Comment $relation.Attributes.Comment + $r.Index = $i++ + $r + } + +} \ No newline at end of file diff --git a/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..9aa8c34e1 --- /dev/null +++ b/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,29 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelation' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-After.json' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + } + + Context 'Get-VSTeamWorkItemRelation' { + It 'Should return relations' { + ## Act + $relation = Get-VSTeamWorkItemRelation -Id 55 + + ## Assert + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/55?api-version=$(_getApiVersion Core)&`$Expand=Relations" + } + $relation.Id | Should -Be 80 + $relation.RelationType | Should -Be "System.LinkTypes.Hierarchy-Reverse" + $relation.Comment | Should -Be "Added from AzFunction" + } + } +} \ No newline at end of file From e982d1772b92266f52d666b5379adbe782bb5819 Mon Sep 17 00:00:00 2001 From: mnieto Date: Thu, 31 Aug 2023 20:29:31 +0200 Subject: [PATCH 13/19] fix test fail introduced in #34c605c0444dc88113cd07766bea8da5765b1c85 --- Tests/library/Cache/RelationTypeCacheTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/library/Cache/RelationTypeCacheTests.cs b/Tests/library/Cache/RelationTypeCacheTests.cs index cb0007420..a1225d0df 100644 --- a/Tests/library/Cache/RelationTypeCacheTests.cs +++ b/Tests/library/Cache/RelationTypeCacheTests.cs @@ -135,7 +135,10 @@ public void RelationTypeCache_Get_ReferenceName() public void RelationTypeCache_Get_ReferenceName_Throws_When_Not_Found() { // Arrange - RelationTypeCache.Update(new Dictionary()); + RelationTypeCache.Update(new Dictionary() { + { "key1", "value1"}, + { "key2", "value2"} + }); // Act var actual = RelationTypeCache.GetReferenceName("NonExistingName"); From 26f7daec10a5d1b862eaf665e697ee60ecd93e47 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 1 Sep 2023 20:41:56 +0200 Subject: [PATCH 14/19] fix test fail introduced in #34c605c0444dc88113cd07766bea8da5765b1c85 --- .../RelationTypeToReferenceNameAttributeTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs b/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs index 462e400b0..ff0fdaa63 100644 --- a/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs +++ b/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs @@ -1,6 +1,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; +using System.Management.Automation; namespace vsteam_lib.Test { @@ -8,6 +11,14 @@ namespace vsteam_lib.Test [ExcludeFromCodeCoverage] public class RelationTypeToReferenceNameAttributeTest { + + private readonly Collection _relationNames = new Collection() { "Produces For", "Predecessor", "Parent" }; + private readonly Collection _relations = new Collection() { + PSObject.AsPSObject(new {Name = "Produces For", ReferenceName = "System.LinkTypes.Remote.Dependency-Forward" }), + PSObject.AsPSObject(new {Name = "Predecessor", ReferenceName = "System.LinkTypes.Dependency-Reverse" }), + PSObject.AsPSObject(new {Name = "Parent", ReferenceName = "System.LinkTypes.Hierarchy-Reverse" }) + }; + [TestMethod] public void RelationTypeTransformToReferenceNameAttribute_Null() { @@ -29,6 +40,11 @@ public void RelationTypeTransformToReferenceNameAttribute_Name_Not_Found() // Arrange var target = new RelationTypeToReferenceNameAttribute(); + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke(this._relations).Returns(this._relationNames); + RelationTypeCache.Cache.Shell = ps; + // Act var actual = target.Transform(null, "NonExistingName"); } From f4e1fe920fbdac851c904acc2558c480abb41be4 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 1 Sep 2023 20:51:03 +0200 Subject: [PATCH 15/19] fix test fails --- Source/Public/Get-VSTeamWorkItemRelation.ps1 | 2 +- Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 | 4 ++++ Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Public/Get-VSTeamWorkItemRelation.ps1 b/Source/Public/Get-VSTeamWorkItemRelation.ps1 index 9b30cb26b..9dc378901 100644 --- a/Source/Public/Get-VSTeamWorkItemRelation.ps1 +++ b/Source/Public/Get-VSTeamWorkItemRelation.ps1 @@ -4,7 +4,7 @@ function Get-VSTeamWorkItemRelation { [int]$Id ) - $relations = Get-VSTeamWorkItem -Id $Id -Expand Relations -Verbose | Select-Object -ExpandProperty Relations + $relations = Get-VSTeamWorkItem -Id $Id -Expand Relations | Select-Object -ExpandProperty Relations $i = 0 foreach ($relation in $relations) { $relatedId = $relation.Url.split("/")[-1] diff --git a/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 index 9aa8c34e1..7bfb815a7 100644 --- a/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 @@ -5,11 +5,15 @@ Describe 'VSTeamWorkItemRelation' { . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/Get-VSTeamWorkItemRelationType.ps1" Mock _getInstance { return 'https://dev.azure.com/test' } Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-After.json' } Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-'; Comment = 'Added from AzFunction' }} -ParameterFilter { $id -eq 80 } + + } Context 'Get-VSTeamWorkItemRelation' { diff --git a/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 index 679861dd4..da6c6e4aa 100644 --- a/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 @@ -25,7 +25,7 @@ Describe 'VSTeamWorkItemRelationType' { $relationTypes = Get-VSTeamWorkItemRelationType -Usage All ## Assert - $relationTypes | Select-Object Usage -Unique | Should -HaveCount 2 + $relationTypes | Select-Object -ExpandProperty attributes | Select-Object Usage -Unique | Should -HaveCount 2 } } } \ No newline at end of file From b05f8a9ee6afef33c9c22816ae2d85129c174e2a Mon Sep 17 00:00:00 2001 From: mnieto Date: Tue, 12 Sep 2023 21:58:00 +0200 Subject: [PATCH 16/19] function Remove-VSTeamWorkItemRelation --- .docs/Remove-VSTeamWorkItemRelation.md | 66 +++++++++++++++++++ .../synopsis/Remove-VSTeamWorkItemRelation.md | 1 + .../Public/Remove-VSTeamWorkItemRelation.ps1 | 29 ++++++++ .../Remove-VSTeamWorkItemRelation.Tests.ps1 | 39 +++++++++++ 4 files changed, 135 insertions(+) create mode 100644 .docs/Remove-VSTeamWorkItemRelation.md create mode 100644 .docs/synopsis/Remove-VSTeamWorkItemRelation.md create mode 100644 Source/Public/Remove-VSTeamWorkItemRelation.ps1 create mode 100644 Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 diff --git a/.docs/Remove-VSTeamWorkItemRelation.md b/.docs/Remove-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..c4c761f2b --- /dev/null +++ b/.docs/Remove-VSTeamWorkItemRelation.md @@ -0,0 +1,66 @@ + + +# Remove-VSTeamWorkItemTag + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + +This cmdlet is a shortcut of Get-VSTeamWorkItem -Id $Id -Expand Relations, and Update-VSTeamWorkItem -Id $Id -Relations $relationsToRemove +Allows easily to remove the relation of 2 workitems just knowing the IDs + +## EXAMPLES + +### Example 1 + +```powershell +55, 66 | Remove-VSTeamWorkItemRelation -FromRelatedId 25 +``` +Imagine 55 and 66 work items are children of 25. This command will remove the Child relationship from 55 to 25 and from 66 to 25 + +### Example 2 + +```powershell +Remove-VSTeamWorkItemRelation -Id 25 -FromRelatedId 55, 66 +``` +Imagine 55 and 66 work items are children of 25. This command will remove the Parent relationship from 25 to 55 and 66 + + +## PARAMETERS + + + +### Id + +Id of the work item + +```yaml +Type: Int32[] +Required: True +Accept pipeline input: true (ByPropertyName, ByValue) + +``` +### FromRelatedId +Id of the related work item +Type: Int32[] +Required: True + +## INPUTS + +## OUTPUTS +PSObject with the updated work item identified by the Id parameter + +## NOTES + + + +## RELATED LINKS + +[Get-VSTeamWorkItem](Get-VSTeamWorkItem.md) +[Get-VSTeamWorkItemRelation](Get-VSTeamWorkItemRelation.md) +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) diff --git a/.docs/synopsis/Remove-VSTeamWorkItemRelation.md b/.docs/synopsis/Remove-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..7fd3a1c92 --- /dev/null +++ b/.docs/synopsis/Remove-VSTeamWorkItemRelation.md @@ -0,0 +1 @@ +Removes the relation from one or more workitems and one or more related workitems \ No newline at end of file diff --git a/Source/Public/Remove-VSTeamWorkItemRelation.ps1 b/Source/Public/Remove-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..ca6364dbd --- /dev/null +++ b/Source/Public/Remove-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,29 @@ +function Remove-VSTeamWorkItemRelation { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium", HelpUri = 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Remove-VSTeamWorkItemRelation')] + param ( + [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [int[]]$Id, + # [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] + # [Parameter(Mandatory, Position=1)] + # [string]$RelationType, + [Parameter(Mandatory, Position=2)] + [int[]]$FromRelatedId, + [switch]$Force + ) + + process { + foreach($item in $Id) { + $relations = Get-VSTeamWorkItemRelation -Id $item + $relationsToRemove = @() + foreach($relatedId in $FromRelatedId) { + $relation = $relations | Where-Object Id -eq $relatedId + if ($relation) { + $relationsToRemove += New-VSTeamWorkItemRelation -Index $relation.Index -Operation Remove + } + } + if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem")) { + Update-VSTeamWorkItem -Id $item -Relations $relationsToRemove + } + } + } +} \ No newline at end of file diff --git a/Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..aad496a33 --- /dev/null +++ b/Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,39 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelation' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/Get-VSTeamWorkItemRelation.ps1" + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + . "$baseFolder/Source/Public/Update-VSTeamWorkItem.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'remove'; Index = '0' }} + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16-After.json' } -ParameterFilter { $Id -eq 16 -and $Relations -eq $null } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16.json' } -ParameterFilter { $Id -eq 16 -and $Relations -ne $null } + + } + + Context 'Remove-VSTeamWorkItemRelation' { + It 'Should remove relations' { + ## Act + $workItem = Remove-VSTeamWorkItemRelation -Id 16 -FromRelatedId 80 + + ## Assert + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/16?api-version=$(_getApiVersion Core)&`$Expand=Relations" + } + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 2 -Scope It + Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Patch' -and + $ContentType -eq 'application/json-patch+json; charset=utf-8' -and + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/16?api-version=$(_getApiVersion Core)" + } + + $workItem.Id | Should -Be 16 + $workItem.Relations | Should -BeNullOrEmpty + } + } +} \ No newline at end of file From af42e5801b603a08dac56b5dd4e1f10f522a4c48 Mon Sep 17 00:00:00 2001 From: mnieto Date: Tue, 12 Sep 2023 22:05:27 +0200 Subject: [PATCH 17/19] fix documentation --- .docs/Remove-VSTeamWorkItemRelation.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.docs/Remove-VSTeamWorkItemRelation.md b/.docs/Remove-VSTeamWorkItemRelation.md index c4c761f2b..2da340c23 100644 --- a/.docs/Remove-VSTeamWorkItemRelation.md +++ b/.docs/Remove-VSTeamWorkItemRelation.md @@ -52,7 +52,6 @@ Required: True ## INPUTS ## OUTPUTS -PSObject with the updated work item identified by the Id parameter ## NOTES From 34cca5a6f8310134d8b052677cf071c7a187bcda Mon Sep 17 00:00:00 2001 From: mnieto Date: Sun, 17 Sep 2023 19:39:37 +0200 Subject: [PATCH 18/19] update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eba601772..1d9175bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 7.14.0 +Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/535) from [Miguel Nieto](https://github.com/mnieto) the following: +- Feat: added work ittems relationship management with the below new/modified CmdLets + - Add-VSTeamWorkItemRelation: Adds a relationship between different workitems + - Get-VSTeamWorkItemRelation: Retrieves a list of relations from a single work item + - Get-VSTeamWorkItemRelationType: Returns a list of the different relation types between work items and links inside the same work item + - New-VSTeamWorkItemRelation: Helper cmdlet that creates an in-memory Relation object to facilitate relationship management + - Remove-VSTeamWorkItemRelation: Removes the relation from one or more workitems and one or more related workitems + - Switch-VSTeamWorkItemParent: Replaces the parent of one or more work items + - Update-VSTeamWorkItem: Added -Relations parameter + + ## 7.13.1 Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/532) from [Miguel Nieto](https://github.com/mnieto) the following: - Fix Set-VSTeamAccount Error on Module Import [531](https://github.com/MethodsAndPractices/vsteam/issues/531) From 1dd735dd64457508f461d3438b3fe64f30db21c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Sch=C3=BCtze?= Date: Sun, 5 Nov 2023 21:25:19 +0100 Subject: [PATCH 19/19] chore: refactored code for readability --- .docs/New-VSTeamWorkItemRelation.md | 10 +-- Source/Public/Add-VSTeamWorkItemRelation.ps1 | 4 +- .../Public/Get-VSTeamWorkItemRelationType.ps1 | 7 +- Source/Public/New-VSTeamWorkItemRelation.ps1 | 77 +++++++++++-------- .../Public/Remove-VSTeamWorkItemRelation.ps1 | 9 +-- Source/Public/Switch-VSTeamWorkItemParent.ps1 | 68 +++++++++------- Source/Public/Update-VSTeamWorkItem.ps1 | 46 ++++++----- .../vsteam_lib.WorkItemRelationType.ps1xml | 6 +- .../Add-VSTeamWorkItemRelation.Tests.ps1 | 2 +- .../New-VSTeamWorkItemRelation.Tests.ps1 | 14 ++-- .../Switch-VSTeamWorkItemParent.Tests.ps1 | 2 +- 11 files changed, 134 insertions(+), 111 deletions(-) diff --git a/.docs/New-VSTeamWorkItemRelation.md b/.docs/New-VSTeamWorkItemRelation.md index 72d1cfc68..e0cf543b5 100644 --- a/.docs/New-VSTeamWorkItemRelation.md +++ b/.docs/New-VSTeamWorkItemRelation.md @@ -30,7 +30,7 @@ Simple invocation, returns a Relation object. ```powershell $relations = New-VSTeamWorkItemRelation -Operation Remove -Index 0 | - New-VSTeamWorkItemRelation -Operation Remove -Index 1 + New-VSTeamWorkItemRelation -Operation Remove -Index 1 Update-VSTeamWorkItem -Id 30 -Relations $relations ``` Removes work first 2 links from work item 30 @@ -70,7 +70,7 @@ Pay attention that this use case, passing a list of work items from pipeline, ha ### Example 6 ```powershell -$relations = Get-VSTeamWiql -Id "f87b028b-0528-47d6-b517-2d82af680295" | +$relations = Get-VSTeamWiql -Id "f87b028b-0528-47d6-b517-2d82af680295" | Select-Object -ExpandProperty WorkItems | New-VSTeamWorkItemRelation -RelationType Related Update-VSTeamWorkItem -Id 30 -Relations $relations @@ -83,14 +83,14 @@ Pay attention that this use case, passing a list of work items from pipeline, ha $relation = New-VSTeamWorkItemRelation -RelationType Related -Operation Replace -Comment "updated comment" Update-VSTeamWorkItem -Id 30 -Relations $relations ``` -Updates the comment of a relation. The replace operation only supports comment update. +Updates the comment of a relation. The replace operation only supports comment update. If you really need to change a relation, like re-parent a work item, you need to create two relations: first, remove and then add operations. ### Example 8 ```powershell $relations =@() $id = Get-VSTeamWorkItem -id 30 -Expand Relation -for ($i=0; $i -lt $id.relations.Count; $i++) { +for ($i=0; $i -lt $id.relations.Count; $i++) { $relations += New-VSTeamWorkItemRelation -Operation Remove -Index $i } Update-VSTeamWorkItem -Id 30 -Relations $relations @@ -99,7 +99,7 @@ Removes all the links from work item 30 ## PARAMETERS -### ImputObject +### InputObject Operation: Intended for fluent syntax (see Example 2) diff --git a/Source/Public/Add-VSTeamWorkItemRelation.ps1 b/Source/Public/Add-VSTeamWorkItemRelation.ps1 index 020e7f093..4a4c98593 100644 --- a/Source/Public/Add-VSTeamWorkItemRelation.ps1 +++ b/Source/Public/Add-VSTeamWorkItemRelation.ps1 @@ -10,10 +10,10 @@ function Add-VSTeamWorkItemRelation { [int]$OfRelatedId, [switch]$Force ) - + process { $relations = $Id | New-VSTeamWorkItemRelation -RelationType $RelationType - if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem")) { + if ($Force -or $pscmdlet.ShouldProcess($Id, "add relations to work items")) { Update-VSTeamWorkItem -Id $OfRelatedId -Relations $relations } } diff --git a/Source/Public/Get-VSTeamWorkItemRelationType.ps1 b/Source/Public/Get-VSTeamWorkItemRelationType.ps1 index 9f168a06d..17db0e6fe 100644 --- a/Source/Public/Get-VSTeamWorkItemRelationType.ps1 +++ b/Source/Public/Get-VSTeamWorkItemRelationType.ps1 @@ -4,15 +4,16 @@ function Get-VSTeamWorkItemRelationType { [ValidateSet('All', 'ResourceLink', 'WorkItemLink')] [string]$Usage = 'WorkItemLink' ) - + process { $commonArgs = @{ area = 'wit' resource = 'workitemrelationtypes' version = $(_getApiVersion Core) noProject = $true - } + } $resp = _callAPI @commonArgs + foreach ($item in $resp.value) { if ($Usage -eq 'All' -or $Usage -eq $item.attributes.usage) { _applyTypesToWorkItemRelationType -item $item @@ -20,5 +21,5 @@ function Get-VSTeamWorkItemRelationType { } } } - + } \ No newline at end of file diff --git a/Source/Public/New-VSTeamWorkItemRelation.ps1 b/Source/Public/New-VSTeamWorkItemRelation.ps1 index 5a7fa852e..18450ba33 100644 --- a/Source/Public/New-VSTeamWorkItemRelation.ps1 +++ b/Source/Public/New-VSTeamWorkItemRelation.ps1 @@ -1,77 +1,88 @@ function New-VSTeamWorkItemRelation { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope="Function", Justification='We still are not doing any persistent changes')] - [CmdletBinding(DefaultParameterSetName="ById", HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/New-VSTeamWorkItemRelation')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope="Function", Justification="No calls against the Azure DevOps API are done. No persistent changes.")] + [CmdletBinding(DefaultParameterSetName="ById", HelpUri="https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/New-VSTeamWorkItemRelation")] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="ByObject")] - [PSCustomObject[]]$ImputObject, + [PSCustomObject[]]$InputObject, + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="ById")] [Parameter(ParameterSetName="ByObject")] [int[]]$Id, + [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] [vsteam_lib.RelationTypeToReferenceNameAttribute()] [Parameter(Mandatory, ParameterSetName="ById")] [Parameter(ParameterSetName="ByObject")] [string]$RelationType, + [ValidateSet('Remove', 'Replace')] [Parameter(Mandatory, ParameterSetName="ByIndex")] [Parameter(ParameterSetName="ByObject")] [string]$Operation, + [Parameter(Mandatory, ParameterSetName="ByIndex")] [Parameter(ParameterSetName="ByObject")] [int]$Index, + [string]$Comment ) process { + + $results = @() + if ($PSCmdlet.ParameterSetName -eq "ByObject") { - $ImputObject + + if ($null -eq $PSBoundParameters.Index) { + + $results += [PSCustomObject]@{ + Id = $Id[0] + RelationType = $RelationType + Operation = "add" + Comment = $Comment + Index = "-" + } + + } else { + + $results += [PSCustomObject]@{ + Id = $null + RelationType = $null + Operation = $Operation.ToLower() + Comment = $Comment + Index = $PSBoundParameters.Index + } + } + } elseif ($PSCmdlet.ParameterSetName -eq "ById") { + foreach ($item in $Id) { - $result = [PSCustomObject]@{ + + $results += [PSCustomObject]@{ Id = $item RelationType = $RelationType Operation = 'add' Comment = $Comment Index = '-' } - _applyTypesToWorkItemRelation $result - $result + } + } else { - $result = [PSCustomObject]@{ + + $results += [PSCustomObject]@{ Id = $null RelationType = $RelationType Operation = $Operation.ToLower() Comment = $Comment Index = $index } - _applyTypesToWorkItemRelation $result - $result + } - } - end { - if ($PSCmdlet.ParameterSetName -eq "ByObject") { - if ($null -eq $PSBoundParameters.Index) { - $result = [PSCustomObject]@{ - Id = $Id[0] - RelationType = $RelationType - Operation = "add" - Comment = $Comment - Index = "-" - } - } else { - $result = [PSCustomObject]@{ - Id = $null - RelationType = $null - Operation = $Operation.ToLower() - Comment = $Comment - Index = $PSBoundParameters.Index - } - } - _applyTypesToWorkItemRelation $result - $result + foreach ($item in $results){ + _applyTypesToWorkItemRelation $item + return $item } } - } \ No newline at end of file diff --git a/Source/Public/Remove-VSTeamWorkItemRelation.ps1 b/Source/Public/Remove-VSTeamWorkItemRelation.ps1 index ca6364dbd..bb3bb3164 100644 --- a/Source/Public/Remove-VSTeamWorkItemRelation.ps1 +++ b/Source/Public/Remove-VSTeamWorkItemRelation.ps1 @@ -3,14 +3,11 @@ function Remove-VSTeamWorkItemRelation { param ( [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [int[]]$Id, - # [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] - # [Parameter(Mandatory, Position=1)] - # [string]$RelationType, [Parameter(Mandatory, Position=2)] [int[]]$FromRelatedId, [switch]$Force ) - + process { foreach($item in $Id) { $relations = Get-VSTeamWorkItemRelation -Id $item @@ -19,9 +16,9 @@ function Remove-VSTeamWorkItemRelation { $relation = $relations | Where-Object Id -eq $relatedId if ($relation) { $relationsToRemove += New-VSTeamWorkItemRelation -Index $relation.Index -Operation Remove - } + } } - if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem")) { + if ($Force -or $pscmdlet.ShouldProcess($Id, "remove relations from work items")) { Update-VSTeamWorkItem -Id $item -Relations $relationsToRemove } } diff --git a/Source/Public/Switch-VSTeamWorkItemParent.ps1 b/Source/Public/Switch-VSTeamWorkItemParent.ps1 index d402b09be..02b46efa4 100644 --- a/Source/Public/Switch-VSTeamWorkItemParent.ps1 +++ b/Source/Public/Switch-VSTeamWorkItemParent.ps1 @@ -8,41 +8,57 @@ function Switch-VSTeamWorkItemParent { [switch]$AddParent, [switch]$Force ) - + process { + + $workItem = $null + foreach($item in $Id) { $ParentNeeded = $false - $wi = Get-VSTeamWorkItem -Id $item -Expand Relations - if ($null -eq $wi.Relations -and $AddParent) { + $workItem = Get-VSTeamWorkItem -Id $item -Expand Relations + + if ($null -eq $workItem.Relations -and $AddParent) { $ParentNeeded = $true - } else { - if ($wi.Relations) { - $relations = $wi | Select-Object -ExpandProperty Relations - $index = 0 - $hasParent = $false - foreach($relation in $relations) { - if ($relation.rel -eq 'System.LinkTypes.Hierarchy-Reverse') { - $hasParent = $true - if ($relation.url.split('/')[-1] -ne $ParentId) { - $patch = New-VSTeamWorkItemRelation -Index $index -Operation Remove | New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent - if ($Force -or $pscmdlet.ShouldProcess($item, "Update-WorkItem")) { - $wi = Update-VSTeamWorkItem -Id $item -Relations $patch + } + elseif ($workItem.Relations) { + + $relations = $workItem | Select-Object -ExpandProperty Relations + $index = 0 + $hasParent = $false + + foreach($relation in $relations) { + + if ($relation.rel -eq 'System.LinkTypes.Hierarchy-Reverse') { + $hasParent = $true + if ($relation.url.split('/')[-1] -ne $ParentId) { + + $patch = New-VSTeamWorkItemRelation -Index $index -Operation Remove | New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent + + if ($Force -or $pscmdlet.ShouldProcess($item, "Update-WorkItem")) { + $workItem = Update-VSTeamWorkItem -Id $item -Relations $patch + } + + break } - break - } + } + $index++ + + } + + if (-not $hasParent -and $AddParent) { + $ParentNeeded = $true } - $index++ - } - if (-not $hasParent -and $AddParent) { - $ParentNeeded = $true } - } - } - if ($ParentNeeded -and ($Foce -or $pscmdlet.ShouldProcess($Id, "Update-WorkItem"))) { - $wi = Update-VSTeamWorkItem -Id $item -Relations (New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent) + + if ($ParentNeeded -and ($Foce -or $pscmdlet.ShouldProcess($Id, "Updates the parent work item"))) { + + $newRelation = New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent + $workItem = Update-VSTeamWorkItem -Id $item -Relations $newRelation + } - $wi + + return $workItem } } } \ No newline at end of file diff --git a/Source/Public/Update-VSTeamWorkItem.ps1 b/Source/Public/Update-VSTeamWorkItem.ps1 index b9ecb9a03..2c8a78018 100644 --- a/Source/Public/Update-VSTeamWorkItem.ps1 +++ b/Source/Public/Update-VSTeamWorkItem.ps1 @@ -18,10 +18,10 @@ function Update-VSTeamWorkItem { [string]$AssignedTo, [Parameter(Mandatory = $false)] - [hashtable]$AdditionalFields, + [PSCustomObject[]]$Relations, [Parameter(Mandatory = $false)] - [PSCustomObject[]]$Relations, + [hashtable]$AdditionalFields, [switch] $Force ) @@ -51,26 +51,6 @@ function Update-VSTeamWorkItem { value = $AssignedTo }) | Where-Object { $_.value } - - #this loop must always come after the main work item fields defined in the function parameters - if ($AdditionalFields) { - foreach ($fieldName in $AdditionalFields.Keys) { - - #check that main properties are not added into the additional fields hashtable - $foundFields = $body | Where-Object { $null -ne $_ -and $_.path -like "*$fieldName" } - if ($null -ne $foundFields) { - throw "Found duplicate field '$fieldName' in parameter AdditionalFields, which is already a parameter. Please remove it." - } - else { - $body += @{ - op = "add" - path = "/fields/$fieldName" - value = $AdditionalFields[$fieldName] - } - } - } - } - foreach ($relation in $Relations) { switch ($relation.Operation) { "add" { @@ -83,7 +63,7 @@ function Update-VSTeamWorkItem { attributes = @{ comment = $relation.Comment } - } + } } } "remove" { @@ -97,11 +77,29 @@ function Update-VSTeamWorkItem { op = $relation.Operation path = "/relations/$($relation.Index)/attributes/comment" value = $relation.Comment - } + } } } } + #this loop must always come after the main work item fields defined in the function parameters + if ($AdditionalFields) { + foreach ($fieldName in $AdditionalFields.Keys) { + + #check that main properties are not added into the additional fields hashtable + $foundFields = $body | Where-Object { $null -ne $_ -and $_.path -like "*$fieldName" } + if ($null -ne $foundFields) { + throw "Found duplicate field '$fieldName' in parameter AdditionalFields, which is already a parameter. Please remove it." + } + else { + $body += @{ + op = "add" + path = "/fields/$fieldName" + value = $AdditionalFields[$fieldName] + } + } + } + } # It is very important that even if the user only provides # a single value above that the item is an array and not diff --git a/Source/types/vsteam_lib.WorkItemRelationType.ps1xml b/Source/types/vsteam_lib.WorkItemRelationType.ps1xml index 14ba046f4..3e82860ae 100644 --- a/Source/types/vsteam_lib.WorkItemRelationType.ps1xml +++ b/Source/types/vsteam_lib.WorkItemRelationType.ps1xml @@ -17,9 +17,9 @@ DefaultDisplayPropertySet - name - referenceName - usage + Name + ReferenceName + Usage Topology diff --git a/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 index 08569720d..18f8e83f3 100644 --- a/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 +++ b/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 @@ -7,7 +7,7 @@ Describe "VSTeamWorkItemRelation" { . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" Mock _getInstance { return 'https://dev.azure.com/test' } - Mock _getApiVersion { return '1.0-unitTests' } #-ParameterFilter { $Service -eq 'Core' } + Mock _getApiVersion { return '1.0-unitTests' } Mock Invoke-RestMethod { Open-SampleFile 'Add-VSTeamWorkItemRelation-Id55.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' -and $Method -eq 'Patch' } Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 70; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 70 } } diff --git a/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 index b813bbf18..e6aa48ee8 100644 --- a/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 +++ b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 @@ -20,7 +20,7 @@ Describe 'VSTeamWorkItemRelation' { $relationsDict.Add($r.Name, $r.ReferenceName) } [vsteam_lib.RelationTypeCache]::Update($relationsDict); - + } Context 'New-VSTeamWorkItemRelation' { @@ -53,32 +53,32 @@ Describe 'VSTeamWorkItemRelation' { It 'With chained calls should return an array of relations' { $actual = New-VSTeamWorkItemRelation -RelationType Child -Id 55 | - New-VSTeamWorkItemRelation -RelationType Child -Id 66 | - New-VSTeamWorkItemRelation -RelationType Child -Id 77 + New-VSTeamWorkItemRelation -RelationType Child -Id 66 | + New-VSTeamWorkItemRelation -RelationType Child -Id 77 $actual | Should -HaveCount 3 $actual[0].Id | Should -Be "55" $actual[1].Id | Should -Be "66" $actual[2].Id | Should -Be "77" - $actual[0].RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + $actual[0].RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" } It 'With WorkItem in the pipeline should use the workItem.Id' { $actual = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Child $actual.Id | Should -Be "55" $actual.Operation | Should -Be "add" - $actual.RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + $actual.RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" } It 'Objects have vsteam_lib.WorkItemRelation TypeName' { $actual = New-VSTeamWorkItemRelation -RelationType Child -Id 55 | - New-VSTeamWorkItemRelation -RelationType Child -Id 66 + New-VSTeamWorkItemRelation -RelationType Child -Id 66 $actual[0].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" $actual[1].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" } It 'With Index paramter ID nor RelationType are needed' { - $actual = New-VSTeamWorkItemRelation -Index 0 -Operation Remove + $actual = New-VSTeamWorkItemRelation -Index 0 -Operation Remove $actual.Id | Should -BeNullOrEmpty $actual.RelationType | Should -BeNullOrEmpty } diff --git a/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 b/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 index a99a6be5b..c7bacb113 100644 --- a/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 +++ b/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 @@ -18,7 +18,7 @@ Describe "VSTeamWorkItemParent" { Mock New-VSTeamWorkItemRelation { return @( [PSCustomObject]@{ Operation = 'remove'; Index = 0 } [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' } - )} -ParameterFilter { $id -eq 80 -and $ImputObject -ne $null} + )} -ParameterFilter { $id -eq 80 -and $InputObject -ne $null} } Context 'Switch-VSTeamWorkItemParent' {