public/items/project_item.ps1
|
Set-MyInvokeCommandAlias -Alias AddItemToProject -Command 'Invoke-AddItemToProject -ProjectId {projectid} -ContentId {contentid}' Set-MyInvokeCommandAlias -Alias RemoveItemFromProject -Command 'Invoke-RemoveItemFromProject -ProjectId {projectid} -ItemId {itemid}' Set-MyInvokeCommandAlias -Alias GetItem -Command 'Invoke-GetItem -ItemId {itemid}' <# .SYNOPSIS Get a project item. .DESCRIPTION Fields will show th emerge between Project and Staged Item fields values .EXAMPLE Get-ProjectItem -Owner "someOwner" -ProjectNumber 164 -ItemId PVTI_lADOBCrGTM4ActQazgMuXXc #> function Get-ProjectItem { [CmdletBinding()] [Alias ("gpi")] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Alias("id")][string]$ItemId, [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter()][switch]$Force ) begin { ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -SkipItems if(! $db){ "Project not found for Owner [$Owner] and ProjectNumber [$ProjectNumber]" | Write-MyError; return $null} # Dirty flag $dirty = $false } process { if(!$db){ return } $item, $dirty = Resolve-ProjectItem -Database $db -ItemId $ItemId -Force:$Force return $item } end { if ($dirty) { "Saving dirty database" | Write-Verbose Save-ProjectDatabaseSafe -Database $db } } } Export-ModuleMember -Function Get-ProjectItem -Alias "gpi" function Get-ProjectItemByUrl{ [CmdletBinding()] [Alias ("gpiu")] param( [Parameter(Mandatory, ValueFromPipeline, Position = 0)][string]$Url, [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter()][switch]$Force, [Parameter()][switch]$PassThru ) begin { ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { return $null } $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -SkipItems if(! $db){ "Project not found for Owner [$Owner] and ProjectNumber [$ProjectNumber]" | Write-MyError; return $null} } process { if(!$db){ return } $item = Get-ItemByUrl -Database $db -Url $Url # TODO: Create a Resolve-ProjectItemByUrl - Depend on function to get item from project remote by url # Get-ItemByUrl only check cache so we need a function that will retreive item from project remote # and update the project cache. # This function depends on the capacity to retreive items by filter # Project API has just been updated to allow search project items if(-not $item){ # "Item not found for URL [$Url]" | Write-MyError return } if($PassThru){ $ret = $item } else { $ret = Format-ProjectItem -Item $item -Attributes @("id", "Title") } return $ret } } Export-ModuleMember -Function Get-ProjectItemByUrl -Alias "gpiu" function Test-ProjectItem { [CmdletBinding()] [Alias ("tpi")] param( [Parameter(Mandatory, ValueFromPipeline, Position = 0)][string]$Url, [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber ) begin { ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber } process { $ret = Test-Item -Database $db -Url $Url return $ret } } Export-ModuleMember -Function Test-ProjectItem -Alias "tpi" function Search-ProjectItem { [CmdletBinding()] [Alias ("spi")] param( [Parameter(Position = 0)] [string[]]$Filter, [Parameter(Position = 1)][string[]]$Attributes, [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter()][switch]$IncludeDone, [Parameter()][switch]$Force, [Parameter()][switch]$PassThru, [Parameter()][string]$FieldName, [Parameter()][switch]$AnyField, [Parameter()][switch]$Exact ) # if $attributes does not contain "Title" add it at the front if(-not ($Attributes -contains "Title")){ $Attributes = @("Title") + $Attributes } # if $attributes does not contain "id" add it at the front if(-not ($Attributes -contains "id")){ $Attributes = @("id") + $Attributes } ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } # Get items as hashtable for later queries $items = Get-ProjectItems -Owner $Owner -ProjectNumber $ProjectNumber -Force:$Force -IncludeDone:$IncludeDone -AsHashtable # return if #items is null if ($null -eq $items) { return $null } if($null -eq $Filter -or $Filter.Count -eq 0){ $found = $items.Values } else { if($AnyField){ if($Exact){ # Exact match in any field $found = $items.Values | Where-Object { Test-WhereExactAnyField -Item $_ -Values $Filter -OR } } else { # Like match in any field $found = $items.Values | Where-Object { Test-WhereLikeAnyField -Item $_ -Values $Filter } } $found = $items.Values | Where-Object { Test-WhereLikeAnyField -Item $_ -Values $Filter } } else { # Default to "Title as the single field to search" $FieldName = [string]::IsNullOrWhiteSpace($FieldName) ? "Title" : $FieldName if($Exact){ # Pick just the first value a in Exact fielname there is only one match Fieldname value $found = $items.Values | Where-Object { Test-WhereExactField -Item $_ -Fieldname $FieldName -Value $Filter[0] } } else { $found = $items.Values | Where-Object { Test-WhereLikeField -Item $_ -Fieldname $FieldName -Values $Filter } } } } if($PassThru){ $ret = $found } else { $ret = $found | Format-ProjectItem -Attributes $Attributes } # If Title is in attributes, sort by title if($Attributes -contains "Title") { $ret = $ret | Sort-Object -Property Title } return $ret } Export-ModuleMember -Function Search-ProjectItem -Alias "spi" function Format-ProjectItem{ [CmdletBinding()] [Alias("fpi")] param( [Parameter(ValueFromPipeline)][object]$Item, [Parameter(Position = 1)][string[]]$Attributes ) begin { if([string]::IsNullOrWhiteSpace($Attributes)){ $Attributes = @("id", "Title") } } process{ $ret = [pscustomobject]::new() foreach($a in $Attributes){ if( ! $Item.$a){ continue } $ret | Add-Member -MemberType NoteProperty -Name $a -Value $Item.$a -force } return $ret } } Export-ModuleMember -Function Format-ProjectItem -Alias "fpi" function Get-ProjectItems { [CmdletBinding()] param( [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter()][switch]$IncludeDone, [Parameter()][switch]$Force, [Parameter()][switch]$AsHashtable ) ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } try { $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -Force:$Force } catch { "Failed to get project [$owner/$ProjectNumber]: $_" | Write-MyError return } # Check if $db is null if($null -eq $db){ "Project not found. Check owner and projectnumber" | Write-MyError ; return } $itemKeys = $db.items.Keys $items = $itemKeys | ForEach-Object { Get-Item $db $_ } # exclude done items if needed if(! $IncludeDone){ $items = $items | Where-Object { $_.Status -ne "Done" } } # return if #items is null if ($null -eq $items) { return } if($AsHashtable){ $ret = New-HashTable foreach($item in $items){ $ret[$item.id] = $item } } else { $ret = @($items | ForEach-Object { [PSCustomObject]$_ }) } return $ret } Export-ModuleMember -Function Get-ProjectItems function Open-ProjectItem { [CmdletBinding()] [Alias ("opi")] param( [Parameter()][string]$Owner, [Parameter()][int]$ProjectNumber, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)] [string]$Id, [Parameter()][switch]$InProject ) begin { ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { throw "Owner and ProjectNumber are required on Open-ProjectItem" } "Project set to [$owner/$ProjectNumber]" | Write-Verbose } process { $itemId = $Id "Opening item [$ItemId] in project [$Owner/$ProjectNumber]" | Write-Verbose $item = Get-ProjectItem -Owner $Owner -ProjectNumber $ProjectNumber -ItemId $ItemId if (-not $item) { throw "Item not found for Owner [$Owner], ProjectNumber [$ProjectNumber] and ItemId [$ItemId]" } if($InProject){ $url = $item.urlPanel } else { # fall back to url if urlcontent is empty $url = $item.urlContent ?? $item.url } if ([string]::IsNullOrWhiteSpace($url)) { # We should never reach this point as all items has a urlpanel set in Convert-NodeItemToHash "No URL found for Item [$ItemId] type [ $($item.type) ]" | Write-Error return } Open-Url -Url $url } } Export-ModuleMember -Function Open-ProjectItem -Alias "opi" <# .SYNOPSIS Edit a project item .EXAMPLE Edit-ProjectItem -Owner "someOwner" -ProjectNumber 164 -Title "Item 1 - title" -FieldName "comment" -Value "new value of the comment" Edit-ProjectItem -Owner "someOwner" -ProjectNumber 164 -Title "Item 1 - title" -FieldName "title" -Value "new value of the title" #> function Edit-ProjectItem { [CmdletBinding()] param( [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter(Mandatory, ValueFromPipeline, Position = 1)][string]$ItemId, [Parameter(Position = 2)][string]$FieldName, [Parameter(Position = 3)][string]$Value, [Parameter()][switch]$Force ) process{ ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } # Force cache update # Full sync if force. Skip items if not force $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -Force:$Force -SkipItems:$(-not $Force) # Find the actual value of the item. Item+Staged # Ignore $dirty as we are changing the db we will always save ($item, $dirty) = Resolve-ProjectItem -Database $db -ItemId $ItemId # if the item is not found if($null -eq $item){ "Item [$ItemId] not found" | Write-MyError; return $null} # Value transformations $valueTransformed = Convertto-ItemTransformedValue -Item $item -Value $Value # Check if value is the same if ( AreEqual -Object1:$item.$FieldName -Object2:$valueTransformed) { "The value is the same, no need to stage it" | Write-Verbose return } # save the new value Save-ItemFieldValue $db $itemId $FieldName $valueTransformed # Commit changes to the database Save-ProjectDatabaseSafe -Database $db } } Export-ModuleMember -Function Edit-ProjectItem function Reset-ProjectItem { [CmdletBinding()] param( [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter(Mandatory, ValueFromPipeline, Position = 1)][string]$ItemId, [Parameter(Position = 2)][string]$FieldName ) process{ ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } # Force cache update # Full sync if force. Skip items if not force $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -Force:$Force -SkipItems:$(-not $Force) # Remove staged if([string]::IsNullOrWhiteSpace($FieldName)){ # Remove all staged changes for the item Remove-ItemStaged $db $ItemId } else { #Remove just the field staged change $field = Get-Field $db $FieldName if([string]::IsNullOrWhiteSpace($field)){ # Field not found throw "Field [$FieldName] not found in project" } else { "Removing staged field [$FieldId] for item [$ItemId] in project [$($db.ProjectId)]" | Write-MyDebug Remove-ItemValueStaged $db $ItemId $field.id } } # Commit changes to the database Save-ProjectDatabaseSafe -Database $db } } Export-ModuleMember -Function Reset-ProjectItem function Add-ProjectItemDirect { [CmdletBinding()] [alias("Add-ProjectItem", "api")] param( [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter(Mandatory, ValueFromPipeline, Position = 0)][string]$Url, [Parameter()][switch]$NoCache ) begin{ ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } # Get project id $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber if ( ! $db) { "Project ID not found for Owner [$Owner] and ProjectNumber [$ProjectNumber]" | Write-MyError return $null } else { $projectId = $db.ProjectId } } process { if(Test-Item $db $Url){ $item = Get-ItemByUrl $db $url # return the id as if has been aded return $item.id } # Get item id $contentId = Get-ContentIdFromUrlDirect -Url $Url if (-not $contentId) { "Content ID not found for URL [$Url]" | Write-MyError return $null } # Add item to project $response = Invoke-MyCommand -Command AddItemToProject -Parameters @{ projectid = $projectId ; contentid = $contentId } $item = $response.data.addProjectV2ItemById.item if ($item) { $ret = $item.id if (! $NoCache) { "Adding item [$ret] to cache" | Write-Verbose $item = $item | Convert-NodeItemToHash Set-Item $db $item Save-ProjectDatabaseSafe -Database $db } return $ret } else { "Item not added to project" | Write-MyError return $null } } } Export-ModuleMember -Function Add-ProjectItemDirect -Alias "Add-ProjectItem", "api" function Remove-ProjectItemDirect { [CmdletBinding(SupportsShouldProcess)] [Alias ("rpi")] param( [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter(Mandatory, ValueFromPipelineByPropertyName,ValueFromPipeline, Position = 0)][Alias("Id")][string]$ItemId, [Parameter()][switch]$Force ) begin { ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } # Get project id $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber if ($db) { $projectId = $db.ProjectId } } process { # With no Project Id we ned to abort if( ! $projectId ){ return } $item = Get-Item $db $ItemId if (-not $item) { "Item [$ItemId] not found in project [$Owner/$ProjectNumber]" | Write-MyHost return } $itemId = $item.id $itemUrl = $item.url try{ # Remove item from project if ($PSCmdlet.ShouldProcess($ItemId, "RRemove from project $Owner/$ProjectNumber")) { $response = Invoke-MyCommand -Command RemoveItemFromProject -Parameters @{ projectid = $projectId ; itemid = $ItemId } } else { # Fake execution return ItemId return $itemUrl } # check if FAILED if ($response.errors -or ($response.data.deleteProjectV2Item.deletedItemId -ne $ItemId)) { "Some issue removing [$ItemId] from project" | Write-MyError if($Force){ "Force flag is set, removing item from cache anyway" | Write-Verbose Remove-Item $db $ItemId Save-ProjectDatabaseSafe -Database $db return $itemUrl } } $result = $response.data.deleteProjectV2Item.deletedItemId if($result -ne $ItemId){ throw "Item [$ItemId] not removed from project properly" } } catch { throw "Error remvoving item [$ItemId] from project: $_" } # Remove item from cache "Removing item [$ItemId] from cache" | Write-Verbose Remove-Item $db $ItemId Save-ProjectDatabaseSafe -Database $db return $itemUrl } } Export-ModuleMember -Function Remove-ProjectItemDirect function Remove-ProjectItem { [CmdletBinding(SupportsShouldProcess)] [Alias ("rpi")] param( [Parameter()][string]$Owner, [Parameter()][string]$ProjectNumber, [Parameter(Mandatory, ValueFromPipelineByPropertyName,ValueFromPipeline, Position = 0)][Alias("Id")][string]$ItemId, [Parameter()][switch]$DeleteIssue, [Parameter()][switch]$Force ) process { # Get item to delete issue later if( ! $DeleteIssue){ # Remove item from project $itemUrl = Remove-ProjectItemDirect -Owner $Owner -ProjectNumber $ProjectNumber -ItemId $ItemId -Force:$Force return $itemUrl } # Find Item to remove $item = Get-ProjectItem -ItemId $ItemId if( ! $item){ "Item [$ItemId] not found, cannot delete issue" | Write-MyWarning return $false } # Remove issue associated with the item # If DraftIssue when it´s already delete when removed from project # If PullRequest. PR can not be deleted if($item.type -ne "Issue") { "Item [$ItemId] is not an Issue, skipping issue deletion" | Write-MyHost return $itemUrl } "Deleting issue associated to item [$ItemId]" | Write-MyDebug if ($item.urlContent) { try { $result = Remove-IssueDirect -Url $item.urlContent } catch { "Issue associated to item [$ItemId] could not be deleted: $_" | Write-MyWarning return $false } } else { "No issue associated to item [$ItemId]" | Write-MyWarning } if($result){ "Issue associated to item [$ItemId] deleted successfully" | Write-Verbose return $true } else { "Issue associated to item [$ItemId] could not be deleted properly" | Write-MyWarning return $false } } } Export-ModuleMember -Function Remove-ProjectItem -Alias "rpi" function Get-ProjectItemDirect { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, Position = 0)][string]$ItemId ) $response = Invoke-MyCommand -Command GetItem -Parameters @{ itemid = $ItemId } # check if the response is null if ($response.errors) { "[$($response.errors[0].type)] $($response.errors[0].message)" | Write-MyError return $null } if ($response.data.node.id -ne $ItemId) { "Item [$ItemId] not found" | Write-MyError return $null } $item = $response.data.node | Convert-NodeItemToHash return $item } Export-ModuleMember -Function Get-ProjectItemDirect function Test-WhereLikeAnyField { param( [Parameter(Mandatory, ValueFromPipeline)] [object]$Item, [Parameter(Mandatory, Position = 0)][string[]]$Values ) process{ foreach ($key in $item.Keys) { if( Test-WhereLikeField -Item $item -Fieldname $key -Values $Values ) { return $true } } return $false } } function Test-WhereExactAnyField { param( [Parameter(Mandatory, ValueFromPipeline)] [object]$Item, [Parameter(Mandatory, Position = 0)][string]$Value ) process{ foreach ($key in $item.Keys) { if( Test-WhereExactField -Item $item -Fieldname $key -Value $Value ) { return $true } } return $false } } function Test-WhereLikeField { param( [Parameter(Mandatory,ValueFromPipeline)] [object]$Item, [Parameter(Mandatory)][string]$FieldName, [Parameter(Mandatory)][string[]]$Values, [Parameter()][switch]$OR ) process { $itemValue = $item.$FieldName $foundCount = 0 foreach ($v in $Values) { if( $itemValue -like "*$v*"){ $foundCount ++ } } return $foundCount -eq $Values.Count } } function Test-WhereExactField { param( [Parameter(Mandatory,ValueFromPipeline)] [object]$Item, [Parameter(Mandatory)][string]$FieldName, [Parameter(Mandatory)][string]$Value, [Parameter()][switch]$OR ) process { $itemValue = $item.$FieldName $ret = $itemValue -eq $Value return $ret } } function AreEqual { param( [object]$Object1, [object]$Object2 ) $Object1 = [string]::IsNullOrEmpty($Object1) ? $null : $Object1 $Object2 = [string]::IsNullOrEmpty($Object2) ? $null : $Object2 # Check if the objects are equal $ret = $Object1 -eq $Object2 return $ret } |